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 e947766b3..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.
@@ -12855,6 +12856,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/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 f11753c8e..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,7 +88,8 @@ 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 == 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/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..efe82ba13 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,47 @@ 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]
+ [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")]
[InlineData("UnknownAction")]
@@ -295,6 +427,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 +504,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/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
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);
}