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()