diff --git a/src/Microsoft.AspNetCore.OData/Query/Container/JsonPropertyNameMapper.cs b/src/Microsoft.AspNetCore.OData/Query/Container/JsonPropertyNameMapper.cs index 695f4ee4c..bb23dff72 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Container/JsonPropertyNameMapper.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Container/JsonPropertyNameMapper.cs @@ -28,7 +28,14 @@ public JsonPropertyNameMapper(IEdmModel model, IEdmStructuredType type) public string MapProperty(string propertyName) { - IEdmProperty property = _type.Properties().Single(s => s.Name == 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); JsonIgnoreAttribute jsonIgnore = GetJsonIgnore(info); diff --git a/src/Microsoft.AspNetCore.OData/Query/ODataQueryOptions.cs b/src/Microsoft.AspNetCore.OData/Query/ODataQueryOptions.cs index a3ea9d1ea..55dac2c69 100644 --- a/src/Microsoft.AspNetCore.OData/Query/ODataQueryOptions.cs +++ b/src/Microsoft.AspNetCore.OData/Query/ODataQueryOptions.cs @@ -364,6 +364,17 @@ public virtual IQueryable ApplyTo(IQueryable query, ODataQuerySettings querySett 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; + } + // 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)) diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ActionResults/ActionResultController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ActionResults/ActionResultController.cs index aea9da5b7..703252dbe 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ActionResults/ActionResultController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ActionResults/ActionResultController.cs @@ -6,6 +6,7 @@ //------------------------------------------------------------------------------ using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OData.Query; @@ -35,5 +36,25 @@ public async Task>> GetCustomers() }, }); } + + private static readonly string[] Summaries = new[] + { + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; + + [HttpGet("/api/weather")] + public IActionResult Get(ODataQueryOptions options) + { + var data = Enumerable.Range(1, 10).Select(index => new Weather + { + Id = index, + TemperatureC = 22 + index, + Summary = Summaries[index - 1] + }) + .ToArray() + .AsQueryable(); + + return Ok(options.ApplyTo(data)); + } } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ActionResults/ActionResultODataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ActionResults/ActionResultODataModel.cs index 0164903fd..12d3f1ddb 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ActionResults/ActionResultODataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ActionResults/ActionResultODataModel.cs @@ -20,4 +20,15 @@ public class Book { public string Id { get; set; } } + + public class Weather + { + public int Id { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + 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 15d12eece..2655fd201 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ActionResults/ActionResultTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ActionResults/ActionResultTests.cs @@ -128,5 +128,57 @@ public async Task ActionResultODataPathReturnsBaseWithoutExpansion() 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); + } } }