diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/Extensions/MapMiddleware.cs b/src/Microsoft.AspNetCore.Http.Abstractions/Extensions/MapMiddleware.cs
index 9d286d39d4..a4f67ce4a2 100644
--- a/src/Microsoft.AspNetCore.Http.Abstractions/Extensions/MapMiddleware.cs
+++ b/src/Microsoft.AspNetCore.Http.Abstractions/Extensions/MapMiddleware.cs
@@ -48,13 +48,15 @@ namespace Microsoft.AspNetCore.Builder.Extensions
throw new ArgumentNullException(nameof(context));
}
- PathString path = context.Request.Path;
+ PathString matchedPath;
PathString remainingPath;
- if (path.StartsWithSegments(_options.PathMatch, out remainingPath))
+
+ if (context.Request.Path.StartsWithSegments(_options.PathMatch, out matchedPath, out remainingPath))
{
// Update the path
- PathString pathBase = context.Request.PathBase;
- context.Request.PathBase = pathBase + _options.PathMatch;
+ var path = context.Request.Path;
+ var pathBase = context.Request.PathBase;
+ context.Request.PathBase = pathBase.Add(matchedPath);
context.Request.Path = remainingPath;
try
diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/Extensions/UsePathBaseExtensions.cs b/src/Microsoft.AspNetCore.Http.Abstractions/Extensions/UsePathBaseExtensions.cs
new file mode 100644
index 0000000000..482f2f481f
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Http.Abstractions/Extensions/UsePathBaseExtensions.cs
@@ -0,0 +1,38 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Builder.Extensions;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ ///
+ /// Extension methods for .
+ ///
+ public static class UsePathBaseExtensions
+ {
+ ///
+ /// Adds a middleware that extracts the specified path base from request path and postpend it to the request path base.
+ ///
+ /// The instance.
+ /// The path base to extract.
+ /// The instance.
+ public static IApplicationBuilder UsePathBase(this IApplicationBuilder app, PathString pathBase)
+ {
+ if (app == null)
+ {
+ throw new ArgumentNullException(nameof(app));
+ }
+
+ // Strip trailing slashes
+ pathBase = pathBase.Value?.TrimEnd('/');
+ if (!pathBase.HasValue)
+ {
+ return app;
+ }
+
+ return app.UseMiddleware(pathBase);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/Extensions/UsePathBaseMiddleware.cs b/src/Microsoft.AspNetCore.Http.Abstractions/Extensions/UsePathBaseMiddleware.cs
new file mode 100644
index 0000000000..6474aeda58
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Http.Abstractions/Extensions/UsePathBaseMiddleware.cs
@@ -0,0 +1,77 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Builder.Extensions
+{
+ ///
+ /// Represents a middleware that extracts the specified path base from request path and postpend it to the request path base.
+ ///
+ public class UsePathBaseMiddleware
+ {
+ private readonly RequestDelegate _next;
+ private readonly PathString _pathBase;
+
+ ///
+ /// Creates a new instace of .
+ ///
+ /// The delegate representing the next middleware in the request pipeline.
+ /// The path base to extract.
+ public UsePathBaseMiddleware(RequestDelegate next, PathString pathBase)
+ {
+ if (next == null)
+ {
+ throw new ArgumentNullException(nameof(next));
+ }
+
+ if (!pathBase.HasValue)
+ {
+ throw new ArgumentException($"{nameof(pathBase)} cannot be null or empty.");
+ }
+
+ _next = next;
+ _pathBase = pathBase;
+ }
+
+ ///
+ /// Executes the middleware.
+ ///
+ /// The for the current request.
+ /// A task that represents the execution of this middleware.
+ public async Task Invoke(HttpContext context)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ PathString matchedPath;
+ PathString remainingPath;
+
+ if (context.Request.Path.StartsWithSegments(_pathBase, out matchedPath, out remainingPath))
+ {
+ var originalPath = context.Request.Path;
+ var originalPathBase = context.Request.PathBase;
+ context.Request.Path = remainingPath;
+ context.Request.PathBase = originalPathBase.Add(matchedPath);
+
+ try
+ {
+ await _next(context);
+ }
+ finally
+ {
+ context.Request.Path = originalPath;
+ context.Request.PathBase = originalPathBase;
+ }
+ }
+ else
+ {
+ await _next(context);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/PathString.cs b/src/Microsoft.AspNetCore.Http.Abstractions/PathString.cs
index 0b56e46292..a211d2737c 100644
--- a/src/Microsoft.AspNetCore.Http.Abstractions/PathString.cs
+++ b/src/Microsoft.AspNetCore.Http.Abstractions/PathString.cs
@@ -203,8 +203,8 @@ namespace Microsoft.AspNetCore.Http
}
///
- /// Determines whether the beginning of this PathString instance matches the specified when compared
- /// using the specified comparison option and returns the remaining segments.
+ /// Determines whether the beginning of this instance matches the specified and returns
+ /// the remaining segments.
///
/// The to compare.
/// The remaining segments after the match.
@@ -215,8 +215,8 @@ namespace Microsoft.AspNetCore.Http
}
///
- /// Determines whether the beginning of this instance matches the specified and returns
- /// the remaining segments.
+ /// Determines whether the beginning of this instance matches the specified when compared
+ /// using the specified comparison option and returns the remaining segments.
///
/// The to compare.
/// One of the enumeration values that determines how this and value are compared.
@@ -238,6 +238,46 @@ namespace Microsoft.AspNetCore.Http
return false;
}
+ ///
+ /// Determines whether the beginning of this instance matches the specified and returns
+ /// the matched and remaining segments.
+ ///
+ /// The to compare.
+ /// The matched segments with the original casing in the source value.
+ /// The remaining segments after the match.
+ /// true if value matches the beginning of this string; otherwise, false.
+ public bool StartsWithSegments(PathString other, out PathString matched, out PathString remaining)
+ {
+ return StartsWithSegments(other, StringComparison.OrdinalIgnoreCase, out matched, out remaining);
+ }
+
+ ///
+ /// Determines whether the beginning of this instance matches the specified when compared
+ /// using the specified comparison option and returns the matched and remaining segments.
+ ///
+ /// The to compare.
+ /// One of the enumeration values that determines how this and value are compared.
+ /// The matched segments with the original casing in the source value.
+ /// The remaining segments after the match.
+ /// true if value matches the beginning of this string; otherwise, false.
+ public bool StartsWithSegments(PathString other, StringComparison comparisonType, out PathString matched, out PathString remaining)
+ {
+ var value1 = Value ?? string.Empty;
+ var value2 = other.Value ?? string.Empty;
+ if (value1.StartsWith(value2, comparisonType))
+ {
+ if (value1.Length == value2.Length || value1[value2.Length] == '/')
+ {
+ matched = new PathString(value1.Substring(0, value2.Length));
+ remaining = new PathString(value1.Substring(value2.Length));
+ return true;
+ }
+ }
+ remaining = Empty;
+ matched = Empty;
+ return false;
+ }
+
///
/// Adds two PathString instances into a combined PathString value.
///
diff --git a/test/Microsoft.AspNetCore.Http.Abstractions.Tests/MapPathMiddlewareTests.cs b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/MapPathMiddlewareTests.cs
index aaee3cc318..a30e99603c 100644
--- a/test/Microsoft.AspNetCore.Http.Abstractions.Tests/MapPathMiddlewareTests.cs
+++ b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/MapPathMiddlewareTests.cs
@@ -75,6 +75,13 @@ namespace Microsoft.AspNetCore.Builder.Extensions
[InlineData("/foo", "/Bar", "/foo/cho/")]
[InlineData("/foo/cho", "/Bar", "/foo/cho")]
[InlineData("/foo/cho", "/Bar", "/foo/cho/do")]
+ [InlineData("/foo", "", "/Foo")]
+ [InlineData("/foo", "", "/Foo/")]
+ [InlineData("/foo", "/Bar", "/Foo")]
+ [InlineData("/foo", "/Bar", "/Foo/Cho")]
+ [InlineData("/foo", "/Bar", "/Foo/Cho/")]
+ [InlineData("/foo/cho", "/Bar", "/Foo/Cho")]
+ [InlineData("/foo/cho", "/Bar", "/Foo/Cho/do")]
public void PathMatchAction_BranchTaken(string matchPath, string basePath, string requestPath)
{
HttpContext context = CreateRequest(basePath, requestPath);
@@ -84,7 +91,7 @@ namespace Microsoft.AspNetCore.Builder.Extensions
app.Invoke(context).Wait();
Assert.Equal(200, context.Response.StatusCode);
- Assert.Equal(basePath + matchPath, context.Items["test.PathBase"]);
+ Assert.Equal(basePath + requestPath.Substring(0, matchPath.Length), (string)context.Items["test.PathBase"]);
Assert.Equal(requestPath.Substring(matchPath.Length), context.Items["test.Path"]);
}
diff --git a/test/Microsoft.AspNetCore.Http.Abstractions.Tests/UsePathBaseExtensionsTests.cs b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/UsePathBaseExtensionsTests.cs
new file mode 100644
index 0000000000..4b8e9d71cb
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/UsePathBaseExtensionsTests.cs
@@ -0,0 +1,168 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder.Internal;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Builder.Extensions
+{
+ public class UsePathBaseExtensionsTests
+ {
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData("/")]
+ public void EmptyOrNullPathBase_DoNotAddMiddleware(string pathBase)
+ {
+ // Arrange
+ var useCalled = false;
+ var builder = new ApplicationBuilderWrapper(CreateBuilder(), () => useCalled = true)
+ .UsePathBase(pathBase);
+
+ // Act
+ builder.Build();
+
+ // Assert
+ Assert.False(useCalled);
+ }
+
+ private class ApplicationBuilderWrapper : IApplicationBuilder
+ {
+ private readonly IApplicationBuilder _wrappedBuilder;
+ private readonly Action _useCallback;
+
+ public ApplicationBuilderWrapper(IApplicationBuilder applicationBuilder, Action useCallback)
+ {
+ _wrappedBuilder = applicationBuilder;
+ _useCallback = useCallback;
+ }
+
+ public IApplicationBuilder Use(Func middleware)
+ {
+ _useCallback();
+ return _wrappedBuilder.Use(middleware);
+ }
+
+ public IServiceProvider ApplicationServices
+ {
+ get { return _wrappedBuilder.ApplicationServices; }
+ set { _wrappedBuilder.ApplicationServices = value; }
+ }
+
+ public IDictionary Properties => _wrappedBuilder.Properties;
+ public IFeatureCollection ServerFeatures => _wrappedBuilder.ServerFeatures;
+ public RequestDelegate Build() => _wrappedBuilder.Build();
+ public IApplicationBuilder New() => _wrappedBuilder.New();
+
+ }
+
+ [Theory]
+ [InlineData("/base", "", "/base", "/base", "")]
+ [InlineData("/base", "", "/base/", "/base", "/")]
+ [InlineData("/base", "", "/base/something", "/base", "/something")]
+ [InlineData("/base", "", "/base/something/", "/base", "/something/")]
+ [InlineData("/base/more", "", "/base/more", "/base/more", "")]
+ [InlineData("/base/more", "", "/base/more/something", "/base/more", "/something")]
+ [InlineData("/base/more", "", "/base/more/something/", "/base/more", "/something/")]
+ [InlineData("/base", "/oldbase", "/base", "/oldbase/base", "")]
+ [InlineData("/base", "/oldbase", "/base/", "/oldbase/base", "/")]
+ [InlineData("/base", "/oldbase", "/base/something", "/oldbase/base", "/something")]
+ [InlineData("/base", "/oldbase", "/base/something/", "/oldbase/base", "/something/")]
+ [InlineData("/base/more", "/oldbase", "/base/more", "/oldbase/base/more", "")]
+ [InlineData("/base/more", "/oldbase", "/base/more/something", "/oldbase/base/more", "/something")]
+ [InlineData("/base/more", "/oldbase", "/base/more/something/", "/oldbase/base/more", "/something/")]
+ public void RequestPathBaseContainingPathBase_IsSplit(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath)
+ {
+ TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath);
+ }
+
+ [Theory]
+ [InlineData("/base", "", "/something", "", "/something")]
+ [InlineData("/base", "", "/baseandsomething", "", "/baseandsomething")]
+ [InlineData("/base", "", "/ba", "", "/ba")]
+ [InlineData("/base", "", "/ba/se", "", "/ba/se")]
+ [InlineData("/base", "/oldbase", "/something", "/oldbase", "/something")]
+ [InlineData("/base", "/oldbase", "/baseandsomething", "/oldbase", "/baseandsomething")]
+ [InlineData("/base", "/oldbase", "/ba", "/oldbase", "/ba")]
+ [InlineData("/base", "/oldbase", "/ba/se", "/oldbase", "/ba/se")]
+ public void RequestPathBaseNotContainingPathBase_IsNotSplit(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath)
+ {
+ TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath);
+ }
+
+ [Theory]
+ [InlineData("", "", "/", "", "/")]
+ [InlineData("/", "", "/", "", "/")]
+ [InlineData("/base", "", "/base/", "/base", "/")]
+ [InlineData("/base/", "", "/base", "/base", "")]
+ [InlineData("/base/", "", "/base/", "/base", "/")]
+ [InlineData("", "/oldbase", "/", "/oldbase", "/")]
+ [InlineData("/", "/oldbase", "/", "/oldbase", "/")]
+ [InlineData("/base", "/oldbase", "/base/", "/oldbase/base", "/")]
+ [InlineData("/base/", "/oldbase", "/base", "/oldbase/base", "")]
+ [InlineData("/base/", "/oldbase", "/base/", "/oldbase/base", "/")]
+ public void PathBaseNeverEndsWithSlash(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath)
+ {
+ TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath);
+ }
+
+ [Theory]
+ [InlineData("/base", "", "/Base/Something", "/Base", "/Something")]
+ [InlineData("/base", "/OldBase", "/Base/Something", "/OldBase/Base", "/Something")]
+ public void PathBaseAndPathPreserveRequestCasing(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath)
+ {
+ TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath);
+ }
+
+ [Theory]
+ [InlineData("/b♫se", "", "/b♫se/something", "/b♫se", "/something")]
+ [InlineData("/b♫se", "", "/B♫se/something", "/B♫se", "/something")]
+ [InlineData("/b♫se", "", "/b♫se/Something", "/b♫se", "/Something")]
+ [InlineData("/b♫se", "/oldb♫se", "/b♫se/something", "/oldb♫se/b♫se", "/something")]
+ [InlineData("/b♫se", "/oldb♫se", "/b♫se/Something", "/oldb♫se/b♫se", "/Something")]
+ [InlineData("/b♫se", "/oldb♫se", "/B♫se/something", "/oldb♫se/B♫se", "/something")]
+ public void PathBaseCanHaveUnicodeCharacters(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath)
+ {
+ TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath);
+ }
+
+ private static void TestPathBase(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath)
+ {
+ HttpContext requestContext = CreateRequest(pathBase, requestPath);
+ var builder = CreateBuilder()
+ .UsePathBase(registeredPathBase);
+ builder.Run(context =>
+ {
+ context.Items["test.Path"] = context.Request.Path;
+ context.Items["test.PathBase"] = context.Request.PathBase;
+ return Task.FromResult(0);
+ });
+ builder.Build().Invoke(requestContext).Wait();
+
+ // Assert path and pathBase are split after middleware
+ Assert.Equal(expectedPath, ((PathString)requestContext.Items["test.Path"]).Value);
+ Assert.Equal(expectedPathBase, ((PathString)requestContext.Items["test.PathBase"]).Value);
+ // Assert path and pathBase are reset after request
+ Assert.Equal(pathBase, requestContext.Request.PathBase.Value);
+ Assert.Equal(requestPath, requestContext.Request.Path.Value);
+ }
+
+ private static HttpContext CreateRequest(string pathBase, string requestPath)
+ {
+ HttpContext context = new DefaultHttpContext();
+ context.Request.PathBase = new PathString(pathBase);
+ context.Request.Path = new PathString(requestPath);
+ return context;
+ }
+
+ private static ApplicationBuilder CreateBuilder()
+ {
+ return new ApplicationBuilder(serviceProvider: null);
+ }
+ }
+}