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

Commit 145d27f

Browse files
committed
Add a PagesOption type that allows configuring the root for Page file discovery
Fixes #5785
1 parent 85e28ae commit 145d27f

File tree

22 files changed

+581
-29
lines changed

22 files changed

+581
-29
lines changed

samples/MvcSandbox/MvcSandbox.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
<Project Sdk="Microsoft.NET.Sdk.Web">
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
22
<Import Project="..\..\build\common.props" />
33

44
<PropertyGroup>
5-
<TargetFrameworks>net452;netcoreapp1.1</TargetFrameworks>
5+
<TargetFrameworks>netcoreapp1.1;net452</TargetFrameworks>
66
<TargetFrameworks Condition="'$(OS)' != 'Windows_NT'">netcoreapp1.1</TargetFrameworks>
77
</PropertyGroup>
88

src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorProject.cs

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using System.IO;
77
using Microsoft.AspNetCore.Razor.Evolution;
88
using Microsoft.Extensions.FileProviders;
9-
using Microsoft.Extensions.Primitives;
109

1110
namespace Microsoft.AspNetCore.Mvc.Razor.Internal
1211
{
@@ -29,21 +28,10 @@ public override RazorProjectItem GetItem(string path)
2928

3029
public override IEnumerable<RazorProjectItem> EnumerateItems(string path)
3130
{
32-
if (path == null)
33-
{
34-
throw new ArgumentNullException(nameof(path));
35-
}
36-
37-
if (path.Length == 0 || path[0] != '/')
38-
{
39-
throw new ArgumentException(Resources.RazorProject_PathMustStartWithForwardSlash);
40-
}
41-
42-
return EnumerateFiles(_provider.GetDirectoryContents(path), path, "");
31+
EnsureValidPath(path);
32+
return EnumerateFiles(_provider.GetDirectoryContents(path), path, prefix: string.Empty);
4333
}
4434

45-
public virtual IChangeToken Watch(string pattern) => _provider.Watch(pattern);
46-
4735
private IEnumerable<RazorProjectItem> EnumerateFiles(IDirectoryContents directory, string basePath, string prefix)
4836
{
4937
if (directory.Exists)
@@ -53,7 +41,7 @@ private IEnumerable<RazorProjectItem> EnumerateFiles(IDirectoryContents director
5341
if (file.IsDirectory)
5442
{
5543
var relativePath = prefix + "/" + file.Name;
56-
var subDirectory = _provider.GetDirectoryContents(relativePath);
44+
var subDirectory = _provider.GetDirectoryContents(JoinPath(basePath, relativePath));
5745
var children = EnumerateFiles(subDirectory, basePath, relativePath);
5846
foreach (var child in children)
5947
{
@@ -67,5 +55,21 @@ private IEnumerable<RazorProjectItem> EnumerateFiles(IDirectoryContents director
6755
}
6856
}
6957
}
58+
59+
private static string JoinPath(string path1, string path2)
60+
{
61+
var hasTrailingSlash = path1.EndsWith("/", StringComparison.Ordinal);
62+
var hasLeadingSlash = path2.StartsWith("/", StringComparison.Ordinal);
63+
if (hasLeadingSlash && hasTrailingSlash)
64+
{
65+
return path1 + path2.Substring(1);
66+
}
67+
else if (hasLeadingSlash || hasTrailingSlash)
68+
{
69+
return path1 + path2;
70+
}
71+
72+
return path1 + "/" + path2;
73+
}
7074
}
7175
}

src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public PageActionDescriptorProvider(
3333

3434
public void OnProvidersExecuting(ActionDescriptorProviderContext context)
3535
{
36-
foreach (var item in _project.EnumerateItems("/"))
36+
foreach (var item in _project.EnumerateItems(_pagesOptions.RootDirectory))
3737
{
3838
if (item.Filename.StartsWith("_"))
3939
{
@@ -66,7 +66,7 @@ public void OnProvidersExecuted(ActionDescriptorProviderContext context)
6666
private void AddActionDescriptors(IList<ActionDescriptor> actions, RazorProjectItem item, string template)
6767
{
6868
var model = new PageApplicationModel(item.CombinedPath, item.PathWithoutExtension);
69-
var routePrefix = item.BasePath == "/" ? item.PathWithoutExtension : item.BasePath + item.PathWithoutExtension;
69+
var routePrefix = item.PathWithoutExtension;
7070
model.Selectors.Add(CreateSelectorModel(routePrefix, template));
7171

7272
if (string.Equals(IndexFileName, item.Filename, StringComparison.OrdinalIgnoreCase))
Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,38 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System;
45
using Microsoft.AspNetCore.Mvc.Infrastructure;
56
using Microsoft.AspNetCore.Mvc.Razor.Internal;
6-
using Microsoft.AspNetCore.Razor.Evolution;
7+
using Microsoft.Extensions.FileProviders;
8+
using Microsoft.Extensions.Options;
79
using Microsoft.Extensions.Primitives;
810

911
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
1012
{
1113
public class PageActionDescriptorChangeProvider : IActionDescriptorChangeProvider
1214
{
13-
private readonly RazorProject _razorProject;
15+
private readonly IFileProvider _fileProvider;
16+
private readonly string _searchPattern;
1417

15-
public PageActionDescriptorChangeProvider(RazorProject razorProject)
18+
public PageActionDescriptorChangeProvider(
19+
IRazorViewEngineFileProviderAccessor fileProviderAccessor,
20+
IOptions<RazorPagesOptions> razorPagesOptions)
1621
{
17-
_razorProject = razorProject;
22+
if (fileProviderAccessor == null)
23+
{
24+
throw new ArgumentNullException(nameof(fileProviderAccessor));
25+
}
26+
27+
if (razorPagesOptions == null)
28+
{
29+
throw new ArgumentNullException(nameof(razorPagesOptions));
30+
}
31+
32+
_fileProvider = fileProviderAccessor.FileProvider;
33+
_searchPattern = razorPagesOptions.Value.RootDirectory.TrimEnd('/') + "/**/*.cshtml";
1834
}
1935

20-
public IChangeToken GetChangeToken() => ((DefaultRazorProject)_razorProject).Watch("**/*.cshtml");
36+
public IChangeToken GetChangeToken() => _fileProvider.Watch(_searchPattern);
2137
}
2238
}

src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs

Lines changed: 18 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Microsoft.AspNetCore.Mvc.RazorPages/RazorPagesOptions.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,34 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
1212
/// </summary>
1313
public class RazorPagesOptions
1414
{
15+
private string _root = "/";
16+
1517
/// <summary>
1618
/// Gets a list of <see cref="IPageApplicationModelConvention"/> instances that will be applied to
1719
/// the <see cref="PageModel"/> when discovering Razor Pages.
1820
/// </summary>
1921
public IList<IPageApplicationModelConvention> Conventions { get; } = new List<IPageApplicationModelConvention>();
22+
23+
/// <summary>
24+
/// Application relative path used as the root of discovery for Razor Page files.
25+
/// </summary>
26+
public string RootDirectory
27+
{
28+
get => _root;
29+
set
30+
{
31+
if (string.IsNullOrEmpty(value))
32+
{
33+
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(value));
34+
}
35+
36+
if (value[0] != '/')
37+
{
38+
throw new ArgumentException(Resources.PathMustBeAnAppRelativePath, nameof(value));
39+
}
40+
41+
_root = value;
42+
}
43+
}
2044
}
2145
}

src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,4 +135,7 @@
135135
<data name="UnsupportedHandlerMethodType" xml:space="preserve">
136136
<value>Unsupported handler method return type '{0}'.</value>
137137
</data>
138+
<data name="PathMustBeAnAppRelativePath" xml:space="preserve">
139+
<value>Path must be an application relative path that starts with a forward slash '/'.</value>
140+
</data>
138141
</root>

test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ public async Task NoPage_NotFound()
3535
public async Task HelloWorld_CanGetContent()
3636
{
3737
// Arrange
38+
// Note: If the route in this test case ever changes, the negative test case
39+
// RazorPagesWithBasePathTest.PageOutsideBasePath_IsNotRouteable needs to be updated too.
3840
var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/HelloWorld");
3941

4042
// Act
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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.Net;
5+
using System.Net.Http;
6+
using System.Threading.Tasks;
7+
using Xunit;
8+
9+
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
10+
{
11+
public class RazorPagesWithBasePathTest : IClassFixture<MvcTestFixture<RazorPagesWebSite.StartupWithBasePath>>
12+
{
13+
public RazorPagesWithBasePathTest(MvcTestFixture<RazorPagesWebSite.StartupWithBasePath> fixture)
14+
{
15+
Client = fixture.Client;
16+
}
17+
18+
public HttpClient Client { get; }
19+
20+
[Fact]
21+
public async Task PageOutsideBasePath_IsNotRouteable()
22+
{
23+
// Act
24+
var response = await Client.GetAsync("/HelloWorld");
25+
26+
// Assert
27+
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
28+
}
29+
30+
[Fact]
31+
public async Task IndexAtBasePath_IsRouteableAtRoot()
32+
{
33+
// Act
34+
var response = await Client.GetAsync("/");
35+
36+
// Assert
37+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
38+
39+
var content = await response.Content.ReadAsStringAsync();
40+
Assert.Equal("Hello from /Index", content.Trim());
41+
}
42+
43+
[Fact]
44+
public async Task IndexAtBasePath_IsRouteableViaIndex()
45+
{
46+
// Act
47+
var response = await Client.GetAsync("/Index");
48+
49+
// Assert
50+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
51+
52+
var content = await response.Content.ReadAsStringAsync();
53+
Assert.Equal("Hello from /Index", content.Trim());
54+
}
55+
56+
[Fact]
57+
public async Task IndexInSubdirectory_IsRouteableViaDirectoryName()
58+
{
59+
// Act
60+
var response = await Client.GetAsync("/Admin/Index");
61+
62+
// Assert
63+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
64+
65+
var content = await response.Content.ReadAsStringAsync();
66+
Assert.Equal("Hello from /Admin/Index", content.Trim());
67+
}
68+
69+
[Fact]
70+
public async Task PageWithRouteTemplateInSubdirectory_IsRouteable()
71+
{
72+
// Act
73+
var response = await Client.GetAsync("/Admin/RouteTemplate/1/MyRouteSuffix/");
74+
75+
// Assert
76+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
77+
78+
var content = await response.Content.ReadAsStringAsync();
79+
Assert.Equal("Hello from /Admin/RouteTemplate 1", content.Trim());
80+
}
81+
82+
[Fact]
83+
public async Task PageWithRouteTemplateInSubdirectory_IsRouteable_WithOptionalParameters()
84+
{
85+
// Act
86+
var response = await Client.GetAsync("/Admin/RouteTemplate/my-user-id/MyRouteSuffix/4");
87+
88+
// Assert
89+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
90+
91+
var content = await response.Content.ReadAsStringAsync();
92+
Assert.Equal("Hello from /Admin/RouteTemplate my-user-id 4", content.Trim());
93+
}
94+
95+
[Fact]
96+
public async Task AuthConvention_IsAppliedOnBasePathRelativePaths_ForFiles()
97+
{
98+
// Act
99+
var response = await Client.GetAsync("/Conventions/Auth");
100+
101+
// Assert
102+
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
103+
Assert.Equal("/Login?ReturnUrl=%2FConventions%2FAuth", response.Headers.Location.PathAndQuery);
104+
}
105+
106+
[Fact]
107+
public async Task AuthConvention_IsAppliedOnBasePathRelativePaths_For_Folders()
108+
{
109+
// Act
110+
var response = await Client.GetAsync("/Conventions/AuthFolder");
111+
112+
// Assert
113+
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
114+
Assert.Equal("/Login?ReturnUrl=%2FConventions%2FAuthFolder", response.Headers.Location.PathAndQuery);
115+
}
116+
}
117+
}

0 commit comments

Comments
 (0)