Add UsePathBase middleware
This commit is contained in:
parent
69729bc75b
commit
e2a0e887af
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IApplicationBuilder"/>.
|
||||
/// </summary>
|
||||
public static class UsePathBaseExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a middleware that extracts the specified path base from request path and postpend it to the request path base.
|
||||
/// </summary>
|
||||
/// <param name="app">The <see cref="IApplicationBuilder"/> instance.</param>
|
||||
/// <param name="pathBase">The path base to extract.</param>
|
||||
/// <returns>The <see cref="IApplicationBuilder"/> instance.</returns>
|
||||
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<UsePathBaseMiddleware>(pathBase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a middleware that extracts the specified path base from request path and postpend it to the request path base.
|
||||
/// </summary>
|
||||
public class UsePathBaseMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly PathString _pathBase;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instace of <see cref="UsePathBaseMiddleware"/>.
|
||||
/// </summary>
|
||||
/// <param name="next">The delegate representing the next middleware in the request pipeline.</param>
|
||||
/// <param name="pathBase">The path base to extract.</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the middleware.
|
||||
/// </summary>
|
||||
/// <param name="context">The <see cref="HttpContext"/> for the current request.</param>
|
||||
/// <returns>A task that represents the execution of this middleware.</returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -203,8 +203,8 @@ namespace Microsoft.AspNetCore.Http
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the beginning of this PathString instance matches the specified <see cref="PathString"/> when compared
|
||||
/// using the specified comparison option and returns the remaining segments.
|
||||
/// Determines whether the beginning of this <see cref="PathString"/> instance matches the specified <see cref="PathString"/> and returns
|
||||
/// the remaining segments.
|
||||
/// </summary>
|
||||
/// <param name="other">The <see cref="PathString"/> to compare.</param>
|
||||
/// <param name="remaining">The remaining segments after the match.</param>
|
||||
|
|
@ -215,8 +215,8 @@ namespace Microsoft.AspNetCore.Http
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the beginning of this <see cref="PathString"/> instance matches the specified <see cref="PathString"/> and returns
|
||||
/// the remaining segments.
|
||||
/// Determines whether the beginning of this <see cref="PathString"/> instance matches the specified <see cref="PathString"/> when compared
|
||||
/// using the specified comparison option and returns the remaining segments.
|
||||
/// </summary>
|
||||
/// <param name="other">The <see cref="PathString"/> to compare.</param>
|
||||
/// <param name="comparisonType">One of the enumeration values that determines how this <see cref="PathString"/> and value are compared.</param>
|
||||
|
|
@ -238,6 +238,46 @@ namespace Microsoft.AspNetCore.Http
|
|||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the beginning of this <see cref="PathString"/> instance matches the specified <see cref="PathString"/> and returns
|
||||
/// the matched and remaining segments.
|
||||
/// </summary>
|
||||
/// <param name="other">The <see cref="PathString"/> to compare.</param>
|
||||
/// <param name="matched">The matched segments with the original casing in the source value.</param>
|
||||
/// <param name="remaining">The remaining segments after the match.</param>
|
||||
/// <returns>true if value matches the beginning of this string; otherwise, false.</returns>
|
||||
public bool StartsWithSegments(PathString other, out PathString matched, out PathString remaining)
|
||||
{
|
||||
return StartsWithSegments(other, StringComparison.OrdinalIgnoreCase, out matched, out remaining);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the beginning of this <see cref="PathString"/> instance matches the specified <see cref="PathString"/> when compared
|
||||
/// using the specified comparison option and returns the matched and remaining segments.
|
||||
/// </summary>
|
||||
/// <param name="other">The <see cref="PathString"/> to compare.</param>
|
||||
/// <param name="comparisonType">One of the enumeration values that determines how this <see cref="PathString"/> and value are compared.</param>
|
||||
/// <param name="matched">The matched segments with the original casing in the source value.</param>
|
||||
/// <param name="remaining">The remaining segments after the match.</param>
|
||||
/// <returns>true if value matches the beginning of this string; otherwise, false.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds two PathString instances into a combined PathString value.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -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"]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<RequestDelegate, RequestDelegate> middleware)
|
||||
{
|
||||
_useCallback();
|
||||
return _wrappedBuilder.Use(middleware);
|
||||
}
|
||||
|
||||
public IServiceProvider ApplicationServices
|
||||
{
|
||||
get { return _wrappedBuilder.ApplicationServices; }
|
||||
set { _wrappedBuilder.ApplicationServices = value; }
|
||||
}
|
||||
|
||||
public IDictionary<string, object> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue