diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs index cd689a6eb..20e3bfacd 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs @@ -248,7 +248,7 @@ await serializer.WriteDeltaObjectInlineAsync(propertyValue, edmProperty.Type, wr } } - private static IEnumerable CreateODataPropertiesFromDynamicType(EdmEntityType entityType, object graph, + private static IEnumerable CreateODataPropertiesFromDynamicType(EdmStructuredType structuredType, object graph, Dictionary dynamicTypeProperties, ODataSerializerContext writeContext) { Contract.Assert(dynamicTypeProperties != null); @@ -267,7 +267,7 @@ private static IEnumerable CreateODataPropertiesFromDynamicType(E { foreach (var prop in dynamicObject.Values) { - IEdmProperty edmProperty = entityType?.Properties() + IEdmProperty edmProperty = structuredType?.Properties() .FirstOrDefault(p => p.Name.Equals(prop.Key, StringComparison.Ordinal)); if (prop.Value != null @@ -321,21 +321,21 @@ private async Task WriteDynamicTypeResourceAsync(object graph, ODataWriter write ODataSerializerContext writeContext) { var dynamicTypeProperties = new Dictionary(); - var entityType = expectedType.Definition as EdmEntityType; + var structuredType = expectedType.Definition as EdmStructuredType; var resource = new ODataResource() { TypeName = expectedType.FullName(), - Properties = CreateODataPropertiesFromDynamicType(entityType, graph, dynamicTypeProperties, writeContext) + Properties = CreateODataPropertiesFromDynamicType(structuredType, graph, dynamicTypeProperties, writeContext) }; resource.IsTransient = true; await writer.WriteStartAsync(resource).ConfigureAwait(false); foreach (var property in dynamicTypeProperties.Keys) { - var resourceContext = new ResourceContext(writeContext, expectedType.AsEntity(), graph); - if (entityType.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 = entityType.NavigationProperties().FirstOrDefault(p => p.Type.Equals(property.Type)); + var navigationProperty = structuredType.NavigationProperties().FirstOrDefault(p => p.Type.Equals(property.Type)); var navigationLink = CreateNavigationLink(navigationProperty, resourceContext); if (navigationLink != null) { diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/EntitySetAggregation/EntitySetAggregationController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/EntitySetAggregation/EntitySetAggregationController.cs index 6067ef6bb..634453865 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/EntitySetAggregation/EntitySetAggregationController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/EntitySetAggregation/EntitySetAggregationController.cs @@ -74,4 +74,32 @@ public void Generate() _context.SaveChanges(); } } + + public class EmployeesController : ODataController + { + private static readonly List employees = new List + { + 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() + { + return employees.AsQueryable(); + } + } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/EntitySetAggregation/EntitySetAggregationDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/EntitySetAggregation/EntitySetAggregationDataModel.cs index c55102a12..37fa4e57a 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/EntitySetAggregation/EntitySetAggregationDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/EntitySetAggregation/EntitySetAggregationDataModel.cs @@ -65,4 +65,21 @@ public class Address public string Street { 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 Location + { + public string City { get; set; } + } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/EntitySetAggregation/EntitySetAggregationTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/EntitySetAggregation/EntitySetAggregationTests.cs index 96d051d96..1a01f70bd 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/EntitySetAggregation/EntitySetAggregationTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/EntitySetAggregation/EntitySetAggregationTests.cs @@ -14,6 +14,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.OData.Edm; +using Microsoft.OData.ModelBuilder; using Newtonsoft.Json.Linq; using Xunit; @@ -202,4 +203,70 @@ public async Task AggregationOnEntitySetWorksWithGroupby() Assert.Equal(2 * (25 + 75), customerOnePrice); } } + + public class NestedComplexPropertyAggregationTests : WebApiTestBase + { + public NestedComplexPropertyAggregationTests(WebApiTestFixture fixture) + : base(fixture) + { + } + + protected static void UpdateConfigureServices(IServiceCollection services) + { + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Employees"); + + services.ConfigureControllers(typeof(EmployeesController)); + + services.AddControllers().AddOData(options => options.Select().Filter().OrderBy().Expand().Count().SkipToken().SetMaxTop(null) + .AddRouteComponents("aggregation", builder.GetEdmModel())); + } + + 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 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()); + } + } }