From 112de85923a1acdb14be9a48a41286ff32014f0f Mon Sep 17 00:00:00 2001 From: Giuliano Barberi Date: Thu, 10 Mar 2022 15:19:10 -0800 Subject: [PATCH 1/2] Adding support for case insensitive action names OData action naming convention can differ from the controller action convention by changing EnableActionNameCaseInsensitive --- .../Microsoft.AspNetCore.OData.xml | 5 + .../PublicAPI.Unshipped.txt | 2 + .../Conventions/OperationRoutingConvention.cs | 7 +- .../Routing/ODataRouteOptions.cs | 5 + ...rosoft.AspNetCore.OData.PublicApi.Net5.bsl | 1 + ...t.AspNetCore.OData.PublicApi.NetCore31.bsl | 1 + .../ActionRoutingConventionTests.cs | 145 ++++++++++++++++++ .../Routing/ODataRouteOptionsTests.cs | 3 + 8 files changed, 168 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml index e947766b3..4eb2dc9dc 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml @@ -12855,6 +12855,11 @@ Initializes a new instance of the class. + + + Gets/sets a value indicating whether to enable case insensitive for the action name in conventional routing. + + Gets/sets a value indicating whether to enable case insensitive for the controller name in conventional routing. diff --git a/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt b/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt index 183104a43..8fa716534 100644 --- a/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt +++ b/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt @@ -1225,6 +1225,8 @@ Microsoft.AspNetCore.OData.Routing.ODataPathSegmentHandler.PathLiteral.get -> st Microsoft.AspNetCore.OData.Routing.ODataPathSegmentTranslator Microsoft.AspNetCore.OData.Routing.ODataPathSegmentTranslator.ODataPathSegmentTranslator() -> void Microsoft.AspNetCore.OData.Routing.ODataRouteOptions +Microsoft.AspNetCore.OData.Routing.ODataRouteOptions.EnableActionNameCaseInsensitive.get -> bool +Microsoft.AspNetCore.OData.Routing.ODataRouteOptions.EnableActionNameCaseInsensitive.set -> void Microsoft.AspNetCore.OData.Routing.ODataRouteOptions.EnableControllerNameCaseInsensitive.get -> bool Microsoft.AspNetCore.OData.Routing.ODataRouteOptions.EnableControllerNameCaseInsensitive.set -> void Microsoft.AspNetCore.OData.Routing.ODataRouteOptions.EnableKeyAsSegment.get -> bool diff --git a/src/Microsoft.AspNetCore.OData/Routing/Conventions/OperationRoutingConvention.cs b/src/Microsoft.AspNetCore.OData/Routing/Conventions/OperationRoutingConvention.cs index f11753c8e..a41228316 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Conventions/OperationRoutingConvention.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Conventions/OperationRoutingConvention.cs @@ -88,7 +88,12 @@ protected void ProcessOperations(ODataControllerActionContext context, IEdmEntit // TODO: refactor here // If we have multiple same function defined, we should match the best one? - IEnumerable candidates = context.Model.SchemaElements.OfType().Where(f => f.IsBound && f.Name == operationName); + StringComparison actionNameComparison = context.Options?.RouteOptions?.EnableActionNameCaseInsensitive switch + { + true => StringComparison.InvariantCultureIgnoreCase, + _ => StringComparison.InvariantCulture + }; + IEnumerable candidates = context.Model.SchemaElements.OfType().Where(f => f.IsBound && string.Equals(f.Name, operationName, actionNameComparison)); foreach (IEdmOperation edmOperation in candidates) { IEdmOperationParameter bindingParameter = edmOperation.Parameters.FirstOrDefault(); diff --git a/src/Microsoft.AspNetCore.OData/Routing/ODataRouteOptions.cs b/src/Microsoft.AspNetCore.OData/Routing/ODataRouteOptions.cs index 4a39a0d4e..eeaad4308 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/ODataRouteOptions.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/ODataRouteOptions.cs @@ -33,6 +33,11 @@ public ODataRouteOptions() _enableUnqualifiedOperationCall = 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. /// diff --git a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net5.bsl b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net5.bsl index c48deba01..a59f8bdc6 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net5.bsl +++ b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net5.bsl @@ -1760,6 +1760,7 @@ public class Microsoft.AspNetCore.OData.Routing.ODataPathSegmentTranslator : Mic public class Microsoft.AspNetCore.OData.Routing.ODataRouteOptions { public ODataRouteOptions () + bool EnableActionNameCaseInsensitive { public get; public set; } bool EnableControllerNameCaseInsensitive { public get; public set; } bool EnableKeyAsSegment { public get; public set; } bool EnableKeyInParenthesis { public get; public set; } diff --git a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.NetCore31.bsl b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.NetCore31.bsl index c48deba01..a59f8bdc6 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.NetCore31.bsl +++ b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.NetCore31.bsl @@ -1760,6 +1760,7 @@ public class Microsoft.AspNetCore.OData.Routing.ODataPathSegmentTranslator : Mic public class Microsoft.AspNetCore.OData.Routing.ODataRouteOptions { public ODataRouteOptions () + bool EnableActionNameCaseInsensitive { public get; public set; } bool EnableControllerNameCaseInsensitive { public get; public set; } bool EnableKeyAsSegment { public get; public set; } bool EnableKeyInParenthesis { public get; public set; } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/ActionRoutingConventionTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/ActionRoutingConventionTests.cs index 9c7eb8939..7e7d7ecd4 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/ActionRoutingConventionTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/ActionRoutingConventionTests.cs @@ -17,14 +17,21 @@ using Microsoft.AspNetCore.OData.Tests.Extensions; using Microsoft.OData.Edm; using Xunit; +using Xunit.Abstractions; namespace Microsoft.AspNetCore.OData.Tests.Routing.Conventions { public class ActionRoutingConventionTests { + private readonly ITestOutputHelper _testOutputHelper; private static ActionRoutingConvention ActionConvention = ConventionHelpers.CreateConvention(); private static IEdmModel EdmModel = GetEdmModel(); + public ActionRoutingConventionTests(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + [Fact] public void AppliesToActionOnActionRoutingConvention_Throws_Context() { @@ -178,6 +185,90 @@ public static TheoryDataSet ActionRoutingConventionTestD }; } } + + public static TheoryDataSet ActionRoutingConventionCaseInsensitiveTestData + { + get + { + return new TheoryDataSet() + { + // Bound to single + { + typeof(CustomersCaseInsensitiveController), + "ISBASEUPGRADED", + new[] + { + "/CustomersCaseInsensitive({key})/NS.IsBaseUpgraded", + "/CustomersCaseInsensitive({key})/IsBaseUpgraded", + "/CustomersCaseInsensitive/{key}/NS.IsBaseUpgraded", + "/CustomersCaseInsensitive/{key}/IsBaseUpgraded", + } + }, + { + typeof(CustomersCaseInsensitiveController), + "ISUPGRADED", + new[] + { + "/CustomersCaseInsensitive({key})/NS.IsUpgraded", + "/CustomersCaseInsensitive({key})/IsUpgraded", + "/CustomersCaseInsensitive/{key}/NS.IsUpgraded", + "/CustomersCaseInsensitive/{key}/IsUpgraded" + } + }, + { + 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 + { + typeof(CustomersCaseInsensitiveController), + "ISBASEALLUPGRADED", + new[] + { + "/CustomersCaseInsensitive/NS.IsBaseAllUpgraded", + "/CustomersCaseInsensitive/IsBaseAllUpgraded" + } + }, + // overload + { + typeof(CustomersCaseInsensitiveController), + "UPGRADEDALLOnCustomer", + new[] + { + "/CustomersCaseInsensitive({key})/NS.UpgradedAll", + "/CustomersCaseInsensitive({key})/UpgradedAll", + "/CustomersCaseInsensitive/{key}/NS.UpgradedAll", + "/CustomersCaseInsensitive/{key}/UpgradedAll" + } + }, + { + typeof(CustomersCaseInsensitiveController), + "UPGRADEDALLOnCollectionOfCustomer", + new[] + { + "/CustomersCaseInsensitive/NS.UpgradedAll", + "/CustomersCaseInsensitive/UpgradedAll" + } + }, + { + typeof(CustomersCaseInsensitiveController), + "UPGRADEDALLOnCollectionOfVipCustomer", + new[] + { + "/CustomersCaseInsensitive/NS.VipCustomer/NS.UpgradedAll", + "/CustomersCaseInsensitive/NS.VipCustomer/UpgradedAll" + } + } + }; + } + } [Theory] [MemberData(nameof(ActionRoutingConventionTestData))] @@ -198,6 +289,28 @@ public void ActionRoutingConventionTestDataRunsAsExpected(Type controllerType, s 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(); + + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Options.RouteOptions.EnableActionNameCaseInsensitive = true; + context.Action = action; + + // Act + ActionConvention.AppliesToAction(context); + + + // Assert + Assert.Equal(templates.Length, action.Selectors.Count); + Assert.Equal(templates, action.Selectors.Select(s => s.AttributeRouteModel.Template)); + } + [Theory] [InlineData("Post")] [InlineData("UnknownAction")] @@ -295,6 +408,7 @@ private static IEdmModel GetEdmModel() EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); container.AddEntitySet("Customers", customer); + container.AddEntitySet("CustomersCaseInsensitive", customer); container.AddSingleton("Me", customer); model.AddElement(container); return model; @@ -371,6 +485,37 @@ public void IsUpgraded(CancellationToken cancellation) public void IsVipUpgraded(ODataActionParameters parameters, string param) { } } + + private class CustomersCaseInsensitiveController + { + [HttpPost] + public void ISBASEUPGRADED(int key, CancellationToken cancellation, ODataActionParameters parameters) + { } + + [HttpPost] + public void ISUPGRADED(int key, CancellationToken cancellation) + { } + + [HttpPost] + public void ISVIPUPGRADED(int key, ODataActionParameters parameters) + { } + + [HttpPost] + public void ISBASEALLUPGRADED(ODataActionParameters parameters) + { } + + [HttpPost] + public void UPGRADEDALLOnCustomer(int key, ODataActionParameters parameters) + { } + + [HttpPost] + public void UPGRADEDALLOnCollectionOfCustomer(ODataActionParameters parameters) + { } + + [HttpPost] + public void UPGRADEDALLOnCollectionOfVipCustomer(ODataActionParameters parameters) + { } + } private class AnotherCustomersController { } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/ODataRouteOptionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/ODataRouteOptionsTests.cs index e8d9ee32a..27e15e035 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/ODataRouteOptionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/ODataRouteOptionsTests.cs @@ -27,6 +27,7 @@ public void DefaultODataRouteOptions_HasDefaultProperties() Assert.True(options.EnableQualifiedOperationCall); Assert.True(options.EnableUnqualifiedOperationCall); Assert.False(options.EnableNonParenthesisForEmptyParameterFunction); + Assert.False(options.EnableActionNameCaseInsensitive); Assert.False(options.EnableControllerNameCaseInsensitive); Assert.False(options.EnablePropertyNameCaseInsensitive); } @@ -43,6 +44,7 @@ public void CtorODataRouteOptions_HasDefaultProperties() Assert.True(options.EnableQualifiedOperationCall); Assert.True(options.EnableUnqualifiedOperationCall); Assert.False(options.EnableNonParenthesisForEmptyParameterFunction); + Assert.False(options.EnableActionNameCaseInsensitive); Assert.False(options.EnableControllerNameCaseInsensitive); Assert.False(options.EnablePropertyNameCaseInsensitive); } @@ -55,6 +57,7 @@ public void ConfigProperties_WorksForEachProperty() 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); } From afeb11bf161efb7dd8983e9c8dc3254c0d8dd60c Mon Sep 17 00:00:00 2001 From: Giuliano Barberi Date: Mon, 14 Mar 2022 14:07:20 -0700 Subject: [PATCH 2/2] Adding case insensitive support to all conventions - Modified EntityRoutingConvention, EntitySetRoutingConvention, and SingletonRoutingConvention - Added tests for all conventions --- .../Edm/EdmModelExtensions.cs | 8 +- .../Microsoft.AspNetCore.OData.xml | 3 +- .../Conventions/EntityRoutingConvention.cs | 2 +- .../Conventions/EntitySetRoutingConvention.cs | 10 +- .../Conventions/OperationRoutingConvention.cs | 10 +- .../Conventions/SingletonRoutingConvention.cs | 15 +- .../ActionRoutingConventionTests.cs | 31 +++- .../EntityRoutingConventionTests.cs | 82 ++++++++++ .../EntitySetRoutingConventionTests.cs | 59 +++++-- .../FunctionRoutingConventionTests.cs | 144 ++++++++++++++++++ .../SingletonRoutingConventionTests.cs | 65 ++++++++ 11 files changed, 388 insertions(+), 41 deletions(-) diff --git a/src/Microsoft.AspNetCore.OData/Edm/EdmModelExtensions.cs b/src/Microsoft.AspNetCore.OData/Edm/EdmModelExtensions.cs index c17211b1e..281c41480 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/EdmModelExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/EdmModelExtensions.cs @@ -392,13 +392,15 @@ public static bool IsEnumOrCollectionEnum(this IEdmTypeReference edmType) /// 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) + 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) == typeName) + if (GetName(baseType).Equals(typeName, typeStringComparison)) { return baseType; } @@ -406,7 +408,7 @@ public static IEdmStructuredType FindTypeInInheritance(this IEdmStructuredType s baseType = baseType.BaseType; } - return model.FindAllDerivedTypes(structuralType).FirstOrDefault(c => GetName(c) == typeName); + return model.FindAllDerivedTypes(structuralType).FirstOrDefault(c => GetName(c).Equals(typeName, typeStringComparison)); } private static string GetName(IEdmStructuredType type) diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml index 4eb2dc9dc..b5aa272f8 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml @@ -2271,13 +2271,14 @@ - + 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. diff --git a/src/Microsoft.AspNetCore.OData/Routing/Conventions/EntityRoutingConvention.cs b/src/Microsoft.AspNetCore.OData/Routing/Conventions/EntityRoutingConvention.cs index 74ba24361..fe56d8378 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Conventions/EntityRoutingConvention.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Conventions/EntityRoutingConvention.cs @@ -74,7 +74,7 @@ public virtual bool AppliesToAction(ODataControllerActionContext context) IEdmStructuredType castType = null; if (castTypeName != null) { - castType = entityType.FindTypeInInheritance(context.Model, castTypeName); + castType = entityType.FindTypeInInheritance(context.Model, castTypeName, context.Options?.RouteOptions?.EnableActionNameCaseInsensitive == true); if (castType == null) { return false; diff --git a/src/Microsoft.AspNetCore.OData/Routing/Conventions/EntitySetRoutingConvention.cs b/src/Microsoft.AspNetCore.OData/Routing/Conventions/EntitySetRoutingConvention.cs index efcad4f13..53c7563a4 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Conventions/EntitySetRoutingConvention.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Conventions/EntitySetRoutingConvention.cs @@ -87,7 +87,7 @@ public virtual bool AppliesToAction(ODataControllerActionContext context) return false; } - IEdmStructuredType castType = entityType.FindTypeInInheritance(context.Model, castTypeName); + IEdmStructuredType castType = entityType.FindTypeInInheritance(context.Model, castTypeName, context.Options?.RouteOptions?.EnableActionNameCaseInsensitive == true); if (castType == null) { return false; @@ -100,7 +100,9 @@ public virtual bool AppliesToAction(ODataControllerActionContext context) private static bool ProcessEntitySetAction(string actionName, IEdmEntitySet entitySet, IEdmStructuredType castType, ODataControllerActionContext context, ActionModel action) { - if (actionName == "Get" || actionName == $"Get{entitySet.Name}") + 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) @@ -141,7 +143,7 @@ private static bool ProcessEntitySetAction(string actionName, IEdmEntitySet enti action.AddSelector("Get", context.Prefix, context.Model, template, context.Options?.RouteOptions); return true; } - else if (actionName == "Post" || actionName == $"Post{entitySet.EntityType().Name}") + else if (actionName.Equals("Post", actionNameComparison) || actionName.Equals($"Post{entitySet.EntityType().Name}", actionNameComparison)) { // POST ~/Customers IList segments = new List @@ -159,7 +161,7 @@ private static bool ProcessEntitySetAction(string actionName, IEdmEntitySet enti action.AddSelector("Post", context.Prefix, context.Model, template, context.Options?.RouteOptions); return true; } - else if (actionName == "Patch" || actionName == $"Patch{entitySet.Name}") + else if (actionName.Equals("Patch", actionNameComparison) || actionName.Equals($"Patch{entitySet.Name}", actionNameComparison)) { // PATCH ~/Patch , ~/PatchCustomers IList segments = new List diff --git a/src/Microsoft.AspNetCore.OData/Routing/Conventions/OperationRoutingConvention.cs b/src/Microsoft.AspNetCore.OData/Routing/Conventions/OperationRoutingConvention.cs index a41228316..4fe40cfb1 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Conventions/OperationRoutingConvention.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Conventions/OperationRoutingConvention.cs @@ -79,7 +79,7 @@ protected void ProcessOperations(ODataControllerActionContext context, IEdmEntit return; } - castTypeFromActionName = entityType.FindTypeInInheritance(context.Model, cast) as IEdmEntityType; + castTypeFromActionName = entityType.FindTypeInInheritance(context.Model, cast, context.Options?.RouteOptions?.EnableActionNameCaseInsensitive == true) as IEdmEntityType; if (castTypeFromActionName == null) { return; @@ -88,12 +88,8 @@ protected void ProcessOperations(ODataControllerActionContext context, IEdmEntit // TODO: refactor here // If we have multiple same function defined, we should match the best one? - StringComparison actionNameComparison = context.Options?.RouteOptions?.EnableActionNameCaseInsensitive switch - { - true => StringComparison.InvariantCultureIgnoreCase, - _ => StringComparison.InvariantCulture - }; - IEnumerable candidates = context.Model.SchemaElements.OfType().Where(f => f.IsBound && string.Equals(f.Name, operationName, actionNameComparison)); + 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(); diff --git a/src/Microsoft.AspNetCore.OData/Routing/Conventions/SingletonRoutingConvention.cs b/src/Microsoft.AspNetCore.OData/Routing/Conventions/SingletonRoutingConvention.cs index 0b606a2e1..8691d4ddb 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Conventions/SingletonRoutingConvention.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Conventions/SingletonRoutingConvention.cs @@ -47,7 +47,7 @@ public bool AppliesToAction(ODataControllerActionContext context) string singletonName = context.Singleton.Name; string actionMethodName = action.ActionName; - if (IsSupportedActionName(actionMethodName, singletonName, out string httpMethod)) + if (IsSupportedActionName(context, actionMethodName, singletonName, out string httpMethod)) { // ~/Me ODataPathTemplate template = new ODataPathTemplate(new SingletonSegmentTemplate(context.Singleton)); @@ -66,7 +66,7 @@ public bool AppliesToAction(ODataControllerActionContext context) } string actionPrefix = actionMethodName.Substring(0, index); - if (IsSupportedActionName(actionPrefix, singletonName, out httpMethod)) + if (IsSupportedActionName(context, actionPrefix, singletonName, out httpMethod)) { string castTypeName = actionMethodName.Substring(index + 4); if (castTypeName.Length == 0) @@ -78,7 +78,7 @@ public bool AppliesToAction(ODataControllerActionContext context) 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); + IEdmStructuredType castType = entityType.FindTypeInInheritance(context.Model, castTypeName, context.Options?.RouteOptions?.EnableActionNameCaseInsensitive == true); if (castType != null) { // ~/Me/Namespace.TypeCast @@ -94,19 +94,20 @@ public bool AppliesToAction(ODataControllerActionContext context) return false; } - private static bool IsSupportedActionName(string actionName, string singletonName, out string httpMethod) + private static bool IsSupportedActionName(ODataControllerActionContext context, string actionName, string singletonName, out string httpMethod) { - if (actionName == "Get" || actionName == $"Get{singletonName}") + 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 == "Put" || actionName == $"Put{singletonName}") + else if (actionName.Equals("Put", actionNameComparison) || actionName.Equals($"Put{singletonName}", actionNameComparison)) { httpMethod = "Put"; return true; } - else if (actionName == "Patch" || actionName == $"Patch{singletonName}") + else if (actionName.Equals("Patch", actionNameComparison) || actionName.Equals($"Patch{singletonName}", actionNameComparison)) { httpMethod = "Patch"; return true; diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/ActionRoutingConventionTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/ActionRoutingConventionTests.cs index 7e7d7ecd4..efe82ba13 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/ActionRoutingConventionTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/ActionRoutingConventionTests.cs @@ -239,7 +239,7 @@ public static TheoryDataSet ActionRoutingConventionCaseI // overload { typeof(CustomersCaseInsensitiveController), - "UPGRADEDALLOnCustomer", + "UPGRADEDALLOnCUSTOMER", new[] { "/CustomersCaseInsensitive({key})/NS.UpgradedAll", @@ -250,7 +250,7 @@ public static TheoryDataSet ActionRoutingConventionCaseI }, { typeof(CustomersCaseInsensitiveController), - "UPGRADEDALLOnCollectionOfCustomer", + "UPGRADEDALLOnCollectionOfCUSTOMER", new[] { "/CustomersCaseInsensitive/NS.UpgradedAll", @@ -259,7 +259,7 @@ public static TheoryDataSet ActionRoutingConventionCaseI }, { typeof(CustomersCaseInsensitiveController), - "UPGRADEDALLOnCollectionOfVipCustomer", + "UPGRADEDALLOnCollectionOfVIPCUSTOMER", new[] { "/CustomersCaseInsensitive/NS.VipCustomer/NS.UpgradedAll", @@ -310,6 +310,25 @@ public void ActionRoutingConventionTestDataWithCaseInsensitiveActionNameRunsAsEx 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); + } [Theory] [InlineData("Post")] @@ -505,15 +524,15 @@ public void ISBASEALLUPGRADED(ODataActionParameters parameters) { } [HttpPost] - public void UPGRADEDALLOnCustomer(int key, ODataActionParameters parameters) + public void UPGRADEDALLOnCUSTOMER(int key, ODataActionParameters parameters) { } [HttpPost] - public void UPGRADEDALLOnCollectionOfCustomer(ODataActionParameters parameters) + public void UPGRADEDALLOnCollectionOfCUSTOMER(ODataActionParameters parameters) { } [HttpPost] - public void UPGRADEDALLOnCollectionOfVipCustomer(ODataActionParameters parameters) + public void UPGRADEDALLOnCollectionOfVIPCUSTOMER(ODataActionParameters parameters) { } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/EntityRoutingConventionTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/EntityRoutingConventionTests.cs index 2d400c32f..2c6db35a2 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/EntityRoutingConventionTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/EntityRoutingConventionTests.cs @@ -123,6 +123,44 @@ public void AppliesToActionForEntityActionWithEntityTypeNameSameAsEntityTypeOnEn 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")] @@ -156,6 +194,36 @@ public void AppliesToActionForEntityActionWithDerivedEntityTypeWorksAsExpected(s 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")] @@ -219,6 +287,7 @@ private static IEdmModel GetEdmModel() EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); container.AddEntitySet("Customers", customer); container.AddEntitySet("AnotherCustomers", customer); + container.AddEntitySet("CaseInsensitiveCustomers", customer); model.AddElement(container); return model; } @@ -277,6 +346,19 @@ 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 0ffc67fb3..0a4d474e7 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/EntitySetRoutingConventionTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/EntitySetRoutingConventionTests.cs @@ -50,16 +50,19 @@ public void AppliesToControllerReturnsExpectedForController(Type controllerType, } [Theory] - [InlineData("Get", "Customers")] - [InlineData("GetCustomersFromVipCustomer", "Customers/NS.VipCustomer")] - public void AppliesToActionForGetActionWorksAsExpected(string actionName, string expectedTemplate) + [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(actionName); + 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(); @@ -78,16 +81,19 @@ public void AppliesToActionForGetActionWorksAsExpected(string actionName, string } [Theory] - [InlineData("Post", "/Customers")] - [InlineData("PostFromVipCustomer", "/Customers/NS.VipCustomer")] - public void AppliesToActionForPostActionWorksAsExpected(string actionName, string expected) + [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(actionName); + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType, actionName); ActionModel action = controller.Actions.First(); ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); context.Action = controller.Actions.First(); + context.Options.RouteOptions.EnableActionNameCaseInsensitive = ignoreCase; EntitySetRoutingConvention entitySetConvention = ConventionHelpers.CreateConvention(); @@ -101,16 +107,19 @@ public void AppliesToActionForPostActionWorksAsExpected(string actionName, strin } [Theory] - [InlineData("Patch", "/Customers")] - [InlineData("PatchCustomers", "/Customers")] - public void AppliesToAction_Works_ForPatchActionWorksAsExpected(string actionName, string expected) + [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(actionName); + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType, actionName); ActionModel action = controller.Actions.First(); ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); context.Action = controller.Actions.First(); + context.Options.RouteOptions.EnableActionNameCaseInsensitive = ignoreCase; EntitySetRoutingConvention entitySetConvention = ConventionHelpers.CreateConvention(); @@ -124,6 +133,7 @@ public void AppliesToAction_Works_ForPatchActionWorksAsExpected(string actionNam } [Theory] + [InlineData("GET")] [InlineData("Get")] [InlineData("PostTo")] [InlineData("GetFrom")] @@ -166,6 +176,7 @@ private static IEdmModel GetEdmModel() EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); container.AddEntitySet("Customers", customer); + container.AddEntitySet("CaseInsensitiveCustomers", customer); container.AddEntitySet("AnotherCustomers", customer); model.AddElement(container); return model; @@ -191,9 +202,33 @@ public void Patch() public void PatchCustomers() { } } + + private class CaseInsensitiveCustomersController + { + public void GET() + { } + + public void GETCASEINSENSITIVECUSTOMERSFromVIPCUSTOMER() + { } + + public void POST() + { } + + public void POSTFromVIPCUSTOMER() + { } + + public void PATCH() + { } + + public void PATCHCASEINSENSITIVECUSTOMERS() + { } + } private class AnotherCustomersController { + public void GET() // Verify case insensitive by default + { } + public void Get(int key) { } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/FunctionRoutingConventionTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/FunctionRoutingConventionTests.cs index 3a7f1e53a..f139d4c40 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/FunctionRoutingConventionTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/FunctionRoutingConventionTests.cs @@ -157,6 +157,78 @@ public static TheoryDataSet FunctionRoutingConventionTes } }; } + } + + public static TheoryDataSet FunctionRoutingConventionCaseInsensitiveTestData + { + get + { + return new TheoryDataSet() + { + // Bound to single + { + typeof(CustomersCaseInsensitiveController), + "ISBASEUPGRADED", + new[] + { + "/CustomersCaseInsensitive({key})/NS.IsBaseUpgraded()", + "/CustomersCaseInsensitive({key})/IsBaseUpgraded()", + "/CustomersCaseInsensitive/{key}/NS.IsBaseUpgraded()", + "/CustomersCaseInsensitive/{key}/IsBaseUpgraded()" + } + }, + { + typeof(CustomersCaseInsensitiveController), + "ISUPGRADED", + new[] + { + "/CustomersCaseInsensitive({key})/NS.IsUpgraded()", + "/CustomersCaseInsensitive({key})/IsUpgraded()", + "/CustomersCaseInsensitive/{key}/NS.IsUpgraded()", + "/CustomersCaseInsensitive/{key}/IsUpgraded()" + } + }, + { + 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 + { + typeof(CustomersCaseInsensitiveController), + "ISBASEALLUPGRADED", + new[] + { + "/CustomersCaseInsensitive/NS.IsBaseAllUpgraded(param={param})", + "/CustomersCaseInsensitive/IsBaseAllUpgraded(param={param})" + } + }, + { + typeof(CustomersCaseInsensitiveController), + "ISALLCUSTOMERSUPGRADED", + new[] + { + "/CustomersCaseInsensitive/NS.IsAllCustomersUpgraded(param={param})", + "/CustomersCaseInsensitive/IsAllCustomersUpgraded(param={param})" + } + }, + { + typeof(CustomersCaseInsensitiveController), + "ISVIPALLUPGRADED", + new[] + { + "/CustomersCaseInsensitive/NS.VipCustomer/NS.IsVipAllUpgraded(param={param})", + "/CustomersCaseInsensitive/NS.VipCustomer/IsVipAllUpgraded(param={param})" + } + } + }; + } } [Theory] @@ -177,6 +249,46 @@ public void FunctionRoutingConventionTestDataRunsAsExpected(Type controllerType, 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(); + + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = action; + context.Options.RouteOptions.EnableActionNameCaseInsensitive = true; + + // Act + FunctionConvention.AppliesToAction(context); + + // 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(); + + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = action; + + // Act + FunctionConvention.AppliesToAction(context); + + // Assert + SelectorModel selector = Assert.Single(action.Selectors); + Assert.Null(selector.AttributeRouteModel); + } public static TheoryDataSet OverloadFunctionTestData { @@ -337,6 +449,7 @@ private static IEdmModel GetEdmModel() EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); container.AddEntitySet("Customers", customer); + container.AddEntitySet("CustomersCaseInsensitive", customer); container.AddSingleton("Me", customer); model.AddElement(container); return model; @@ -414,6 +527,37 @@ public void IsUpgraded(CancellationToken cancellation) public void IsVipUpgraded(string param) { } } + + private class CustomersCaseInsensitiveController + { + public void GET() + { } + + [HttpGet] + public void ISBASEUPGRADED(int key, CancellationToken cancellation) + { } + + [HttpGet] + public void ISUPGRADED(int key, CancellationToken cancellation) + { } + + [HttpGet] + public void ISVIPUPGRADED(int key, string param) + { } + + [HttpGet] + public void ISBASEALLUPGRADED(int param) + { } + + [HttpGet] + public void ISALLCUSTOMERSUPGRADED(int param) + { } + + [HttpGet] + public void ISVIPALLUPGRADED(CancellationToken cancellation, int param) + { } + } + private class AnotherCustomersController { } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/SingletonRoutingConventionTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/SingletonRoutingConventionTests.cs index 1dd13870d..40ff8a435 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/SingletonRoutingConventionTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/SingletonRoutingConventionTests.cs @@ -71,6 +71,23 @@ public static TheoryDataSet SingletonConventionTestData }; } } + + public static TheoryDataSet SingletonConventionCaseInsensitiveTestData + { + 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" } }, + }; + } + } [Theory] [MemberData(nameof(SingletonConventionTestData))] @@ -90,6 +107,26 @@ public void SingletonRoutingConventionTestDataRunsAsExpected(string actionName, 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(); + + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = action; + context.Options.RouteOptions.EnableActionNameCaseInsensitive = true; + + // Act + SingletonConvention.AppliesToAction(context); + + // Assert + Assert.Equal(templates.Length, action.Selectors.Count); + Assert.Equal(templates, action.Selectors.Select(s => s.AttributeRouteModel.Template)); + } [Theory] [InlineData("GetFrom")] @@ -124,6 +161,7 @@ private static IEdmModel GetEdmModel() model.AddElement(vipCustomer); var entityContainer = new EdmEntityContainer("NS", "Default"); entityContainer.AddSingleton("Me", customer); + entityContainer.AddSingleton("CaseInsensitiveMe", customer); model.AddElement(entityContainer); return model; } @@ -165,6 +203,33 @@ public void PutFrom() public void PatchFrom() { } + } + + private class CaseInsensitiveMeController + { + public void GET() + { + } + + public void GETFromVIPCUSTOMER() + { + } + + public void PUT() + { + } + + public void PUTFromVIPCUSTOMER() + { + } + + public void PATCH() + { + } + + public void PATCHFromVIPCUSTOMER() + { + } } private class CustomersController