From 3b0b4a2f3a5ae946b5c080dddc831b0dfeb230b9 Mon Sep 17 00:00:00 2001 From: John Gathogo Date: Wed, 27 Apr 2022 12:22:34 +0300 Subject: [PATCH] Support FromBody parameters with actions and functions --- .../Formatter/ODataOutputFormatterHelper.cs | 26 +------------ .../Routing/Template/ActionSegmentTemplate.cs | 11 +++++- .../Template/FunctionSegmentTemplate.cs | 11 +++++- .../Template/SegmentTemplateHelpers.cs | 21 ++++++++++ .../Serialization/ComplexTypeTests.cs | 4 ++ .../Template/ActionSegmentTemplateTests.cs | 38 +++++++++++++++++++ .../Template/FunctionSegmentTemplateTests.cs | 36 ++++++++++++++++++ 7 files changed, 119 insertions(+), 28 deletions(-) diff --git a/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatterHelper.cs b/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatterHelper.cs index 351158d9f..57d28ab60 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatterHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatterHelper.cs @@ -66,7 +66,7 @@ internal static async Task WriteToStreamAsync( IODataSerializer serializer = GetSerializer(type, value, request, serializerProvider); ODataPath path = request.ODataFeature().Path; - IEdmNavigationSource targetNavigationSource = GetTargetNavigationSource(path, model); + IEdmNavigationSource targetNavigationSource = path.GetNavigationSource(); HttpResponse response = request.HttpContext.Response; // serialize a response @@ -196,30 +196,6 @@ internal static IODataSerializer GetSerializer(Type type, object value, HttpRequ return serializer; } - private static IEdmNavigationSource GetTargetNavigationSource(ODataPath path, IEdmModel model) - { - if (path == null) - { - return null; - } - - Contract.Assert(model != null); - - OperationSegment operationSegment = path.LastSegment as OperationSegment; - if (operationSegment != null) - { - // OData model builder uses an annotation to save the function returned entity set. - // TODO: we need to refactor it later. - ReturnedEntitySetAnnotation entitySetAnnotation = model.GetAnnotationValue(operationSegment.Operations.Single()); - if (entitySetAnnotation != null) - { - return model.EntityContainer.FindEntitySet(entitySetAnnotation.EntitySetName); - } - } - - return path.GetNavigationSource(); - } - private static string GetRootElementName(ODataPath path) { if (path != null) diff --git a/src/Microsoft.AspNetCore.OData/Routing/Template/ActionSegmentTemplate.cs b/src/Microsoft.AspNetCore.OData/Routing/Template/ActionSegmentTemplate.cs index c339c79f1..0329c8cf5 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Template/ActionSegmentTemplate.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Template/ActionSegmentTemplate.cs @@ -10,6 +10,7 @@ using System.Linq; using Microsoft.OData; using Microsoft.OData.Edm; +using Microsoft.OData.ModelBuilder; using Microsoft.OData.UriParser; namespace Microsoft.AspNetCore.OData.Routing.Template @@ -104,7 +105,15 @@ public override bool TryTranslate(ODataTemplateTranslateContext context) throw Error.ArgumentNull(nameof(context)); } - context.Segments.Add(Segment); + 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); return true; } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Template/FunctionSegmentTemplate.cs b/src/Microsoft.AspNetCore.OData/Routing/Template/FunctionSegmentTemplate.cs index 0afe7ccd2..ea1a676a3 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Template/FunctionSegmentTemplate.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Template/FunctionSegmentTemplate.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.OData.Edm; using Microsoft.OData; using Microsoft.OData.Edm; +using Microsoft.OData.ModelBuilder; using Microsoft.OData.UriParser; namespace Microsoft.AspNetCore.OData.Routing.Template @@ -144,10 +145,16 @@ public override bool TryTranslate(ODataTemplateTranslateContext context) throw Error.ArgumentNull(nameof(context)); } + IEdmNavigationSource navigationSource = NavigationSource; + if (navigationSource == null) + { + navigationSource = SegmentTemplateHelpers.GetNavigationSourceFromEdmOperation(context.Model, Function); + } + // 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)); + context.Segments.Add(new OperationSegment(Function, navigationSource as IEdmEntitySetBase)); return true; } @@ -172,7 +179,7 @@ public override bool TryTranslate(ODataTemplateTranslateContext context) return false; } - context.Segments.Add(new OperationSegment(Function, parameters, NavigationSource as IEdmEntitySetBase)); + context.Segments.Add(new OperationSegment(Function, parameters, navigationSource as IEdmEntitySetBase)); return true; } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Template/SegmentTemplateHelpers.cs b/src/Microsoft.AspNetCore.OData/Routing/Template/SegmentTemplateHelpers.cs index 46e44a38f..8535398aa 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Template/SegmentTemplateHelpers.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Template/SegmentTemplateHelpers.cs @@ -15,6 +15,7 @@ using Microsoft.AspNetCore.Routing; using Microsoft.OData; using Microsoft.OData.Edm; +using Microsoft.OData.ModelBuilder; using Microsoft.OData.UriParser; namespace Microsoft.AspNetCore.OData.Routing.Template @@ -173,5 +174,25 @@ internal static bool IsMatchParameters(RouteValueDictionary routeValues, IDictio // 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) + { + ReturnedEntitySetAnnotation entitySetAnnotation = model?.GetAnnotationValue(operation); + + if (entitySetAnnotation != null) + { + return model.EntityContainer.FindEntitySet(entitySetAnnotation.EntitySetName); + } + + return null; + } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ComplexTypeTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ComplexTypeTests.cs index 52dbe205b..2367b2652 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ComplexTypeTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ComplexTypeTests.cs @@ -5,6 +5,7 @@ // //------------------------------------------------------------------------------ +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.OData.Extensions; using Microsoft.AspNetCore.OData.Formatter.MediaType; @@ -13,6 +14,7 @@ using Microsoft.OData; using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; +using Microsoft.OData.UriParser; using Xunit; namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization @@ -26,6 +28,8 @@ public async Task ComplexTypeSerializesAsOData() 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; diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/ActionSegmentTemplateTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/ActionSegmentTemplateTests.cs index c3f73a0c8..4556e5338 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/ActionSegmentTemplateTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/ActionSegmentTemplateTests.cs @@ -8,12 +8,17 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.OData.Routing; using Microsoft.AspNetCore.OData.Routing.Template; using Microsoft.AspNetCore.OData.Tests.Commons; +using Microsoft.AspNetCore.Routing; using Microsoft.OData; using Microsoft.OData.Edm; +using Microsoft.OData.ModelBuilder; using Microsoft.OData.UriParser; +using Moq; using Xunit; namespace Microsoft.AspNetCore.OData.Tests.Routing.Template @@ -142,5 +147,38 @@ public void TryTranslateActionSegmentTemplate_ReturnsODataActionImportSegment() 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/FunctionSegmentTemplateTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/FunctionSegmentTemplateTests.cs index 3c18a2019..399bc6910 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/FunctionSegmentTemplateTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/FunctionSegmentTemplateTests.cs @@ -8,12 +8,15 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.OData.Routing; using Microsoft.AspNetCore.OData.Routing.Template; using Microsoft.AspNetCore.OData.Tests.Commons; using Microsoft.AspNetCore.Routing; using Microsoft.OData; using Microsoft.OData.Edm; +using Microsoft.OData.ModelBuilder; using Microsoft.OData.UriParser; using Moq; using Xunit; @@ -404,5 +407,38 @@ public void TryTranslateFunctionSegmentTemplate_ReturnsODataFunctionSegment_Usin Assert.Equal("name", parameter.Name); Assert.Equal("Ji/Change# T", 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); + } } }