diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml index b5aa272f8..b6c01f6a4 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml @@ -12427,13 +12427,14 @@ The Edm entity type. The Edm navigation source. - + 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. diff --git a/src/Microsoft.AspNetCore.OData/Routing/Conventions/OperationRoutingConvention.cs b/src/Microsoft.AspNetCore.OData/Routing/Conventions/OperationRoutingConvention.cs index 4fe40cfb1..a31a81851 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Conventions/OperationRoutingConvention.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Conventions/OperationRoutingConvention.cs @@ -65,31 +65,17 @@ protected void ProcessOperations(ODataControllerActionContext context, IEdmEntit return; } - // OperationNameOnCollectionOfEntityType - string operationName = SplitActionName(actionName, out string cast, out bool isOnCollection); - + bool isOnCollection = false; IEdmEntityType castTypeFromActionName = null; - if (cast != null) - { - if (cast.Length == 0) - { - // Early return for the following cases: - // - {OperationName}On - // - {OperationName}OnCollectionOf - return; - } - castTypeFromActionName = entityType.FindTypeInInheritance(context.Model, cast, context.Options?.RouteOptions?.EnableActionNameCaseInsensitive == true) as IEdmEntityType; - if (castTypeFromActionName == null) - { - return; - } + IEdmOperation[] candidates = FindCandidates(context, actionName); + if (candidates.Length == 0) + { + // 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); } - // 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; - IEnumerable candidates = context.Model.SchemaElements.OfType().Where(f => f.IsBound && f.Name.Equals(operationName, actionNameComparison)); foreach (IEdmOperation edmOperation in candidates) { IEdmOperationParameter bindingParameter = edmOperation.Parameters.FirstOrDefault(); @@ -172,14 +158,62 @@ protected void ProcessOperations(ODataControllerActionContext context, IEdmEntit } } + 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; + + return context.Model.SchemaElements + .OfType() + .Where(f => f.IsBound && f.Name.Equals(operationName, actionNameComparison)) + .ToArray(); + } + + 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; + + string operationName = SplitActionName(actionName, out string cast, out isOnCollection, caseComparision); + + castTypeFromActionName = null; + if (cast != null) + { + if (cast.Length == 0) + { + // Early return for the following cases: + // - {OperationName}On + // - {OperationName}OnCollectionOf + return Array.Empty(); + } + + castTypeFromActionName = entityType.FindTypeInInheritance(context.Model, cast, context.Options?.RouteOptions?.EnableActionNameCaseInsensitive == true) as IEdmEntityType; + if (castTypeFromActionName == null) + { + return Array.Empty(); + } + } + + return FindCandidates(context, operationName); + } + /// /// 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) + internal static string SplitActionName(string actionName, out string cast, out bool isOnCollection, + StringComparison comparison = StringComparison.Ordinal) { Contract.Assert(actionName != null); @@ -190,7 +224,7 @@ internal static string SplitActionName(string actionName, out string cast, out b cast = null; isOnCollection = false; string operation; - int index = actionName.IndexOf("OnCollectionOf", StringComparison.Ordinal); + int index = actionName.LastIndexOf("OnCollectionOf", comparison); if (index > 0) { operation = actionName.Substring(0, index); @@ -199,7 +233,7 @@ internal static string SplitActionName(string actionName, out string cast, out b return operation; } - index = actionName.IndexOf("On", StringComparison.Ordinal); + index = actionName.LastIndexOf("On", comparison); if (index > 0) { operation = actionName.Substring(0, index); diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/FunctionRoutingConventionTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/FunctionRoutingConventionTests.cs index 34d95d5a9..618d11fad 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/FunctionRoutingConventionTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/FunctionRoutingConventionTests.cs @@ -154,11 +154,47 @@ public static TheoryDataSet FunctionRoutingConventionTes "/Customers/NS.GetWholeSalary(minSalary={minSalary},maxSalary={maxSalary},aveSalary={aveSalary})", "/Customers/GetWholeSalary(minSalary={minSalary},maxSalary={maxSalary},aveSalary={aveSalary})" } + }, + { + typeof(CustomersController), + "GetStatusOnLineOfflineUser", + new[] + { + "/Customers/NS.GetStatusOnLineOfflineUser()", + "/Customers/GetStatusOnLineOfflineUser()" + } + }, + { + typeof(CustomersController), + "GetStatusOnLineOfflineUser", + new[] + { + "/Customers/NS.GetStatusOnLineOfflineUser()", + "/Customers/GetStatusOnLineOfflineUser()" + } + }, + { + typeof(CustomersController), + "StatusLineOfflineUserOn", + new[] + { + "/Customers/NS.StatusLineOfflineUserOn()", + "/Customers/StatusLineOfflineUserOn()" + } + }, + { + typeof(CustomersController), + "GetStatusOnLineOfflineUserOnVipCustomer", + new[] + { + "/Customers/NS.VipCustomer/NS.GetStatusOnLineOfflineUser(param={param})", + "/Customers/NS.VipCustomer/GetStatusOnLineOfflineUser(param={param})" + } } }; } - } - + } + public static TheoryDataSet FunctionRoutingConventionCaseInsensitiveTestData { get @@ -448,6 +484,19 @@ private static IEdmModel GetEdmModel() 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); @@ -461,6 +510,15 @@ private class CustomersController public void Get() { } + [HttpGet] + public void GetStatusOnLineOfflineUser() { } + + [HttpGet] + public void GetStatusOnLineOfflineUserOnVipCustomer(int param) { } + + [HttpGet] + public void StatusLineOfflineUserOn() { } + [HttpGet] public void IsBaseUpgraded(int key, CancellationToken cancellation) { }