Skip to content
This repository was archived by the owner on Nov 20, 2018. It is now read-only.

Commit e2a0e88

Browse files
committed
Add UsePathBase middleware
1 parent 69729bc commit e2a0e88

File tree

6 files changed

+341
-9
lines changed

6 files changed

+341
-9
lines changed

src/Microsoft.AspNetCore.Http.Abstractions/Extensions/MapMiddleware.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,15 @@ public async Task Invoke(HttpContext context)
4848
throw new ArgumentNullException(nameof(context));
4949
}
5050

51-
PathString path = context.Request.Path;
51+
PathString matchedPath;
5252
PathString remainingPath;
53-
if (path.StartsWithSegments(_options.PathMatch, out remainingPath))
53+
54+
if (context.Request.Path.StartsWithSegments(_options.PathMatch, out matchedPath, out remainingPath))
5455
{
5556
// Update the path
56-
PathString pathBase = context.Request.PathBase;
57-
context.Request.PathBase = pathBase + _options.PathMatch;
57+
var path = context.Request.Path;
58+
var pathBase = context.Request.PathBase;
59+
context.Request.PathBase = pathBase.Add(matchedPath);
5860
context.Request.Path = remainingPath;
5961

6062
try
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Microsoft.AspNetCore.Http;
6+
using Microsoft.AspNetCore.Builder.Extensions;
7+
8+
namespace Microsoft.AspNetCore.Builder
9+
{
10+
/// <summary>
11+
/// Extension methods for <see cref="IApplicationBuilder"/>.
12+
/// </summary>
13+
public static class UsePathBaseExtensions
14+
{
15+
/// <summary>
16+
/// Adds a middleware that extracts the specified path base from request path and postpend it to the request path base.
17+
/// </summary>
18+
/// <param name="app">The <see cref="IApplicationBuilder"/> instance.</param>
19+
/// <param name="pathBase">The path base to extract.</param>
20+
/// <returns>The <see cref="IApplicationBuilder"/> instance.</returns>
21+
public static IApplicationBuilder UsePathBase(this IApplicationBuilder app, PathString pathBase)
22+
{
23+
if (app == null)
24+
{
25+
throw new ArgumentNullException(nameof(app));
26+
}
27+
28+
// Strip trailing slashes
29+
pathBase = pathBase.Value?.TrimEnd('/');
30+
if (!pathBase.HasValue)
31+
{
32+
return app;
33+
}
34+
35+
return app.UseMiddleware<UsePathBaseMiddleware>(pathBase);
36+
}
37+
}
38+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Threading.Tasks;
6+
using Microsoft.AspNetCore.Http;
7+
8+
namespace Microsoft.AspNetCore.Builder.Extensions
9+
{
10+
/// <summary>
11+
/// Represents a middleware that extracts the specified path base from request path and postpend it to the request path base.
12+
/// </summary>
13+
public class UsePathBaseMiddleware
14+
{
15+
private readonly RequestDelegate _next;
16+
private readonly PathString _pathBase;
17+
18+
/// <summary>
19+
/// Creates a new instace of <see cref="UsePathBaseMiddleware"/>.
20+
/// </summary>
21+
/// <param name="next">The delegate representing the next middleware in the request pipeline.</param>
22+
/// <param name="pathBase">The path base to extract.</param>
23+
public UsePathBaseMiddleware(RequestDelegate next, PathString pathBase)
24+
{
25+
if (next == null)
26+
{
27+
throw new ArgumentNullException(nameof(next));
28+
}
29+
30+
if (!pathBase.HasValue)
31+
{
32+
throw new ArgumentException($"{nameof(pathBase)} cannot be null or empty.");
33+
}
34+
35+
_next = next;
36+
_pathBase = pathBase;
37+
}
38+
39+
/// <summary>
40+
/// Executes the middleware.
41+
/// </summary>
42+
/// <param name="context">The <see cref="HttpContext"/> for the current request.</param>
43+
/// <returns>A task that represents the execution of this middleware.</returns>
44+
public async Task Invoke(HttpContext context)
45+
{
46+
if (context == null)
47+
{
48+
throw new ArgumentNullException(nameof(context));
49+
}
50+
51+
PathString matchedPath;
52+
PathString remainingPath;
53+
54+
if (context.Request.Path.StartsWithSegments(_pathBase, out matchedPath, out remainingPath))
55+
{
56+
var originalPath = context.Request.Path;
57+
var originalPathBase = context.Request.PathBase;
58+
context.Request.Path = remainingPath;
59+
context.Request.PathBase = originalPathBase.Add(matchedPath);
60+
61+
try
62+
{
63+
await _next(context);
64+
}
65+
finally
66+
{
67+
context.Request.Path = originalPath;
68+
context.Request.PathBase = originalPathBase;
69+
}
70+
}
71+
else
72+
{
73+
await _next(context);
74+
}
75+
}
76+
}
77+
}

src/Microsoft.AspNetCore.Http.Abstractions/PathString.cs

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,8 @@ public bool StartsWithSegments(PathString other, StringComparison comparisonType
203203
}
204204

205205
/// <summary>
206-
/// Determines whether the beginning of this PathString instance matches the specified <see cref="PathString"/> when compared
207-
/// using the specified comparison option and returns the remaining segments.
206+
/// Determines whether the beginning of this <see cref="PathString"/> instance matches the specified <see cref="PathString"/> and returns
207+
/// the remaining segments.
208208
/// </summary>
209209
/// <param name="other">The <see cref="PathString"/> to compare.</param>
210210
/// <param name="remaining">The remaining segments after the match.</param>
@@ -215,8 +215,8 @@ public bool StartsWithSegments(PathString other, out PathString remaining)
215215
}
216216

217217
/// <summary>
218-
/// Determines whether the beginning of this <see cref="PathString"/> instance matches the specified <see cref="PathString"/> and returns
219-
/// the remaining segments.
218+
/// Determines whether the beginning of this <see cref="PathString"/> instance matches the specified <see cref="PathString"/> when compared
219+
/// using the specified comparison option and returns the remaining segments.
220220
/// </summary>
221221
/// <param name="other">The <see cref="PathString"/> to compare.</param>
222222
/// <param name="comparisonType">One of the enumeration values that determines how this <see cref="PathString"/> and value are compared.</param>
@@ -238,6 +238,46 @@ public bool StartsWithSegments(PathString other, StringComparison comparisonType
238238
return false;
239239
}
240240

241+
/// <summary>
242+
/// Determines whether the beginning of this <see cref="PathString"/> instance matches the specified <see cref="PathString"/> and returns
243+
/// the matched and remaining segments.
244+
/// </summary>
245+
/// <param name="other">The <see cref="PathString"/> to compare.</param>
246+
/// <param name="matched">The matched segments with the original casing in the source value.</param>
247+
/// <param name="remaining">The remaining segments after the match.</param>
248+
/// <returns>true if value matches the beginning of this string; otherwise, false.</returns>
249+
public bool StartsWithSegments(PathString other, out PathString matched, out PathString remaining)
250+
{
251+
return StartsWithSegments(other, StringComparison.OrdinalIgnoreCase, out matched, out remaining);
252+
}
253+
254+
/// <summary>
255+
/// Determines whether the beginning of this <see cref="PathString"/> instance matches the specified <see cref="PathString"/> when compared
256+
/// using the specified comparison option and returns the matched and remaining segments.
257+
/// </summary>
258+
/// <param name="other">The <see cref="PathString"/> to compare.</param>
259+
/// <param name="comparisonType">One of the enumeration values that determines how this <see cref="PathString"/> and value are compared.</param>
260+
/// <param name="matched">The matched segments with the original casing in the source value.</param>
261+
/// <param name="remaining">The remaining segments after the match.</param>
262+
/// <returns>true if value matches the beginning of this string; otherwise, false.</returns>
263+
public bool StartsWithSegments(PathString other, StringComparison comparisonType, out PathString matched, out PathString remaining)
264+
{
265+
var value1 = Value ?? string.Empty;
266+
var value2 = other.Value ?? string.Empty;
267+
if (value1.StartsWith(value2, comparisonType))
268+
{
269+
if (value1.Length == value2.Length || value1[value2.Length] == '/')
270+
{
271+
matched = new PathString(value1.Substring(0, value2.Length));
272+
remaining = new PathString(value1.Substring(value2.Length));
273+
return true;
274+
}
275+
}
276+
remaining = Empty;
277+
matched = Empty;
278+
return false;
279+
}
280+
241281
/// <summary>
242282
/// Adds two PathString instances into a combined PathString value.
243283
/// </summary>

test/Microsoft.AspNetCore.Http.Abstractions.Tests/MapPathMiddlewareTests.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@ public void PathMatchFunc_BranchTaken(string matchPath, string basePath, string
7575
[InlineData("/foo", "/Bar", "/foo/cho/")]
7676
[InlineData("/foo/cho", "/Bar", "/foo/cho")]
7777
[InlineData("/foo/cho", "/Bar", "/foo/cho/do")]
78+
[InlineData("/foo", "", "/Foo")]
79+
[InlineData("/foo", "", "/Foo/")]
80+
[InlineData("/foo", "/Bar", "/Foo")]
81+
[InlineData("/foo", "/Bar", "/Foo/Cho")]
82+
[InlineData("/foo", "/Bar", "/Foo/Cho/")]
83+
[InlineData("/foo/cho", "/Bar", "/Foo/Cho")]
84+
[InlineData("/foo/cho", "/Bar", "/Foo/Cho/do")]
7885
public void PathMatchAction_BranchTaken(string matchPath, string basePath, string requestPath)
7986
{
8087
HttpContext context = CreateRequest(basePath, requestPath);
@@ -84,7 +91,7 @@ public void PathMatchAction_BranchTaken(string matchPath, string basePath, strin
8491
app.Invoke(context).Wait();
8592

8693
Assert.Equal(200, context.Response.StatusCode);
87-
Assert.Equal(basePath + matchPath, context.Items["test.PathBase"]);
94+
Assert.Equal(basePath + requestPath.Substring(0, matchPath.Length), (string)context.Items["test.PathBase"]);
8895
Assert.Equal(requestPath.Substring(matchPath.Length), context.Items["test.Path"]);
8996
}
9097

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Threading.Tasks;
7+
using Microsoft.AspNetCore.Builder.Internal;
8+
using Microsoft.AspNetCore.Http;
9+
using Microsoft.AspNetCore.Http.Features;
10+
using Xunit;
11+
12+
namespace Microsoft.AspNetCore.Builder.Extensions
13+
{
14+
public class UsePathBaseExtensionsTests
15+
{
16+
[Theory]
17+
[InlineData(null)]
18+
[InlineData("")]
19+
[InlineData("/")]
20+
public void EmptyOrNullPathBase_DoNotAddMiddleware(string pathBase)
21+
{
22+
// Arrange
23+
var useCalled = false;
24+
var builder = new ApplicationBuilderWrapper(CreateBuilder(), () => useCalled = true)
25+
.UsePathBase(pathBase);
26+
27+
// Act
28+
builder.Build();
29+
30+
// Assert
31+
Assert.False(useCalled);
32+
}
33+
34+
private class ApplicationBuilderWrapper : IApplicationBuilder
35+
{
36+
private readonly IApplicationBuilder _wrappedBuilder;
37+
private readonly Action _useCallback;
38+
39+
public ApplicationBuilderWrapper(IApplicationBuilder applicationBuilder, Action useCallback)
40+
{
41+
_wrappedBuilder = applicationBuilder;
42+
_useCallback = useCallback;
43+
}
44+
45+
public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
46+
{
47+
_useCallback();
48+
return _wrappedBuilder.Use(middleware);
49+
}
50+
51+
public IServiceProvider ApplicationServices
52+
{
53+
get { return _wrappedBuilder.ApplicationServices; }
54+
set { _wrappedBuilder.ApplicationServices = value; }
55+
}
56+
57+
public IDictionary<string, object> Properties => _wrappedBuilder.Properties;
58+
public IFeatureCollection ServerFeatures => _wrappedBuilder.ServerFeatures;
59+
public RequestDelegate Build() => _wrappedBuilder.Build();
60+
public IApplicationBuilder New() => _wrappedBuilder.New();
61+
62+
}
63+
64+
[Theory]
65+
[InlineData("/base", "", "/base", "/base", "")]
66+
[InlineData("/base", "", "/base/", "/base", "/")]
67+
[InlineData("/base", "", "/base/something", "/base", "/something")]
68+
[InlineData("/base", "", "/base/something/", "/base", "/something/")]
69+
[InlineData("/base/more", "", "/base/more", "/base/more", "")]
70+
[InlineData("/base/more", "", "/base/more/something", "/base/more", "/something")]
71+
[InlineData("/base/more", "", "/base/more/something/", "/base/more", "/something/")]
72+
[InlineData("/base", "/oldbase", "/base", "/oldbase/base", "")]
73+
[InlineData("/base", "/oldbase", "/base/", "/oldbase/base", "/")]
74+
[InlineData("/base", "/oldbase", "/base/something", "/oldbase/base", "/something")]
75+
[InlineData("/base", "/oldbase", "/base/something/", "/oldbase/base", "/something/")]
76+
[InlineData("/base/more", "/oldbase", "/base/more", "/oldbase/base/more", "")]
77+
[InlineData("/base/more", "/oldbase", "/base/more/something", "/oldbase/base/more", "/something")]
78+
[InlineData("/base/more", "/oldbase", "/base/more/something/", "/oldbase/base/more", "/something/")]
79+
public void RequestPathBaseContainingPathBase_IsSplit(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath)
80+
{
81+
TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath);
82+
}
83+
84+
[Theory]
85+
[InlineData("/base", "", "/something", "", "/something")]
86+
[InlineData("/base", "", "/baseandsomething", "", "/baseandsomething")]
87+
[InlineData("/base", "", "/ba", "", "/ba")]
88+
[InlineData("/base", "", "/ba/se", "", "/ba/se")]
89+
[InlineData("/base", "/oldbase", "/something", "/oldbase", "/something")]
90+
[InlineData("/base", "/oldbase", "/baseandsomething", "/oldbase", "/baseandsomething")]
91+
[InlineData("/base", "/oldbase", "/ba", "/oldbase", "/ba")]
92+
[InlineData("/base", "/oldbase", "/ba/se", "/oldbase", "/ba/se")]
93+
public void RequestPathBaseNotContainingPathBase_IsNotSplit(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath)
94+
{
95+
TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath);
96+
}
97+
98+
[Theory]
99+
[InlineData("", "", "/", "", "/")]
100+
[InlineData("/", "", "/", "", "/")]
101+
[InlineData("/base", "", "/base/", "/base", "/")]
102+
[InlineData("/base/", "", "/base", "/base", "")]
103+
[InlineData("/base/", "", "/base/", "/base", "/")]
104+
[InlineData("", "/oldbase", "/", "/oldbase", "/")]
105+
[InlineData("/", "/oldbase", "/", "/oldbase", "/")]
106+
[InlineData("/base", "/oldbase", "/base/", "/oldbase/base", "/")]
107+
[InlineData("/base/", "/oldbase", "/base", "/oldbase/base", "")]
108+
[InlineData("/base/", "/oldbase", "/base/", "/oldbase/base", "/")]
109+
public void PathBaseNeverEndsWithSlash(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath)
110+
{
111+
TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath);
112+
}
113+
114+
[Theory]
115+
[InlineData("/base", "", "/Base/Something", "/Base", "/Something")]
116+
[InlineData("/base", "/OldBase", "/Base/Something", "/OldBase/Base", "/Something")]
117+
public void PathBaseAndPathPreserveRequestCasing(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath)
118+
{
119+
TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath);
120+
}
121+
122+
[Theory]
123+
[InlineData("/b♫se", "", "/b♫se/something", "/b♫se", "/something")]
124+
[InlineData("/b♫se", "", "/B♫se/something", "/B♫se", "/something")]
125+
[InlineData("/b♫se", "", "/b♫se/Something", "/b♫se", "/Something")]
126+
[InlineData("/b♫se", "/oldb♫se", "/b♫se/something", "/oldb♫se/b♫se", "/something")]
127+
[InlineData("/b♫se", "/oldb♫se", "/b♫se/Something", "/oldb♫se/b♫se", "/Something")]
128+
[InlineData("/b♫se", "/oldb♫se", "/B♫se/something", "/oldb♫se/B♫se", "/something")]
129+
public void PathBaseCanHaveUnicodeCharacters(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath)
130+
{
131+
TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath);
132+
}
133+
134+
private static void TestPathBase(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath)
135+
{
136+
HttpContext requestContext = CreateRequest(pathBase, requestPath);
137+
var builder = CreateBuilder()
138+
.UsePathBase(registeredPathBase);
139+
builder.Run(context =>
140+
{
141+
context.Items["test.Path"] = context.Request.Path;
142+
context.Items["test.PathBase"] = context.Request.PathBase;
143+
return Task.FromResult(0);
144+
});
145+
builder.Build().Invoke(requestContext).Wait();
146+
147+
// Assert path and pathBase are split after middleware
148+
Assert.Equal(expectedPath, ((PathString)requestContext.Items["test.Path"]).Value);
149+
Assert.Equal(expectedPathBase, ((PathString)requestContext.Items["test.PathBase"]).Value);
150+
// Assert path and pathBase are reset after request
151+
Assert.Equal(pathBase, requestContext.Request.PathBase.Value);
152+
Assert.Equal(requestPath, requestContext.Request.Path.Value);
153+
}
154+
155+
private static HttpContext CreateRequest(string pathBase, string requestPath)
156+
{
157+
HttpContext context = new DefaultHttpContext();
158+
context.Request.PathBase = new PathString(pathBase);
159+
context.Request.Path = new PathString(requestPath);
160+
return context;
161+
}
162+
163+
private static ApplicationBuilder CreateBuilder()
164+
{
165+
return new ApplicationBuilder(serviceProvider: null);
166+
}
167+
}
168+
}

0 commit comments

Comments
 (0)