diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml
index c4768d167..cddd485ef 100644
--- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml
+++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml
@@ -6190,6 +6190,13 @@
The setup config.
The built service provider.
+
+
+ Sanitizes the route prefix by stripping leading and trailing forward slashes.
+
+ Route prefix to sanitize.
+ Sanitized route prefix.
+
Sets up default options for .
diff --git a/src/Microsoft.AspNetCore.OData/ODataOptions.cs b/src/Microsoft.AspNetCore.OData/ODataOptions.cs
index aa33b186d..953b6370f 100644
--- a/src/Microsoft.AspNetCore.OData/ODataOptions.cs
+++ b/src/Microsoft.AspNetCore.OData/ODataOptions.cs
@@ -7,6 +7,7 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Diagnostics.Contracts;
using Microsoft.AspNetCore.OData.Abstracts;
using Microsoft.AspNetCore.OData.Batch;
@@ -129,14 +130,22 @@ public ODataOptions AddRouteComponents(string routePrefix, IEdmModel model, Acti
throw Error.ArgumentNull(nameof(model));
}
- if (RouteComponents.ContainsKey(routePrefix))
+ if (routePrefix == null)
{
- throw Error.InvalidOperation(SRResources.ModelPrefixAlreadyUsed, routePrefix);
+ throw Error.ArgumentNull(nameof(routePrefix));
}
+ string sanitizedRoutePrefix = SanitizeRoutePrefix(routePrefix);
+
+ if (RouteComponents.ContainsKey(sanitizedRoutePrefix))
+ {
+ throw Error.InvalidOperation(SRResources.ModelPrefixAlreadyUsed, sanitizedRoutePrefix);
+ }
+
+
// Consider to use Lazy ?
IServiceProvider serviceProvider = BuildRouteContainer(model, configureServices);
- RouteComponents[routePrefix] = (model, serviceProvider);
+ RouteComponents[sanitizedRoutePrefix] = (model, serviceProvider);
return this;
}
@@ -147,9 +156,16 @@ public ODataOptions AddRouteComponents(string routePrefix, IEdmModel model, Acti
/// The root service provider for the route (prefix) name.
public IServiceProvider GetRouteServices(string routePrefix)
{
- if (routePrefix != null && RouteComponents.ContainsKey(routePrefix))
+ if (routePrefix == null)
+ {
+ return null;
+ }
+
+ string sanitizedRoutePrefix = SanitizeRoutePrefix(routePrefix);
+
+ if (RouteComponents.TryGetValue(sanitizedRoutePrefix, out var components))
{
- return RouteComponents[routePrefix].ServiceProvider;
+ return components.ServiceProvider;
}
return null;
@@ -300,5 +316,22 @@ private IServiceProvider BuildRouteContainer(IEdmModel model, Action
+ /// Sanitizes the route prefix by stripping leading and trailing forward slashes.
+ ///
+ /// Route prefix to sanitize.
+ /// Sanitized route prefix.
+ private string SanitizeRoutePrefix(string routePrefix)
+ {
+ Debug.Assert(routePrefix != null);
+
+ if (routePrefix.Length > 0 && routePrefix[0] != '/' && routePrefix[^1] != '/')
+ {
+ return routePrefix;
+ }
+
+ return routePrefix.Trim('/');
+ }
}
}
diff --git a/test/Microsoft.AspNetCore.OData.Tests/ODataOptionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/ODataOptionsTests.cs
index 5b60a6274..3c072cba1 100644
--- a/test/Microsoft.AspNetCore.OData.Tests/ODataOptionsTests.cs
+++ b/test/Microsoft.AspNetCore.OData.Tests/ODataOptionsTests.cs
@@ -140,6 +140,25 @@ public void AddRouteComponents_WithDependencyInjection_SetModelAndServices()
Assert.IsType(actual);
}
+ [Theory]
+ [InlineData("/odata", "odata")]
+ [InlineData("/odata/", "odata")]
+ [InlineData("odata/", "odata")]
+ [InlineData("/", "")]
+ public void AddRouteComponents_Strips_RoutePrefix_Leading_And_Trailing_Slashes(string routePrefix, string expectedRoutePrefix)
+ {
+ // Arrange
+ ODataOptions options = new ODataOptions();
+ IEdmModel edmModel = EdmCoreModel.Instance;
+
+ // Act
+ options.AddRouteComponents(routePrefix, edmModel, services => services.AddSingleton());
+
+ // Assert
+ Assert.False(options.RouteComponents.ContainsKey(routePrefix));
+ Assert.True(options.RouteComponents.ContainsKey(expectedRoutePrefix));
+ }
+
[Fact]
public void AddRouteComponents_Throws_IfModelNull()
{
@@ -150,6 +169,16 @@ public void AddRouteComponents_Throws_IfModelNull()
ExceptionAssert.ThrowsArgumentNull(() => options.AddRouteComponents("odata", null, builder => { }), "model");
}
+ [Fact]
+ public void AddRouteComponents_Throws_IfRoutePrefixNull()
+ {
+ // Arrange
+ ODataOptions options = new ODataOptions();
+
+ // Act & Assert
+ ExceptionAssert.ThrowsArgumentNull(() => options.AddRouteComponents(null, EdmCoreModel.Instance, builder => { }), "routePrefix");
+ }
+
[Fact]
public void AddRouteComponents_Throws_IfPrefixExisted()
{
@@ -192,6 +221,30 @@ public void GetRouteServices_ReturnsCorrectServiceProvider()
Assert.NotNull(sp);
}
+ [Theory]
+ [InlineData("/odata")]
+ [InlineData("/odata/")]
+ [InlineData("odata/")]
+ public void GetRouteServices_ReturnsCorrectServiceProvider_When_Leading_Or_Trailing_Slashes(string routePrefix)
+ {
+ // Arrange
+ ODataOptions options = new ODataOptions();
+ IEdmModel edmModel = EdmCoreModel.Instance;
+
+ // Act
+ options.AddRouteComponents(routePrefix, edmModel);
+
+ // & Assert
+ // can retrieve service provider using original routePrefix
+ IServiceProvider sp = options.GetRouteServices(routePrefix);
+ Assert.NotNull(sp);
+
+ // can retrieve service provider using sanitized routePrefix
+ string sanitizedRoutePrefix = "odata";
+ IServiceProvider sp2 = options.GetRouteServices(sanitizedRoutePrefix);
+ Assert.NotNull(sp2);
+ }
+
#region QuerySetting
[Fact]
public void SetMaxTop_Throws_ForWrongValue()