Normalize paths returned by view location expanders

Fixes #6448
This commit is contained in:
Pranav K 2017-06-26 16:02:54 -07:00
parent 3536cf5aad
commit d1813a7cd7
16 changed files with 193 additions and 138 deletions

View File

@ -8,6 +8,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
{ {
public class CompiledViewDescriptor public class CompiledViewDescriptor
{ {
/// <summary>
/// The normalized application relative path of the view.
/// </summary>
public string RelativePath { get; set; } public string RelativePath { get; set; }
/// <summary> /// <summary>

View File

@ -57,11 +57,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
var pageFactory = Expression var pageFactory = Expression
.Lambda<Func<IRazorPage>>(objectInitializeExpression) .Lambda<Func<IRazorPage>>(objectInitializeExpression)
.Compile(); .Compile();
return new RazorPageFactoryResult(pageFactory, viewDescriptor.ExpirationTokens, viewDescriptor.IsPrecompiled); return new RazorPageFactoryResult(viewDescriptor, pageFactory);
} }
else else
{ {
return new RazorPageFactoryResult(viewDescriptor.ExpirationTokens); return new RazorPageFactoryResult(viewDescriptor, razorPageFactory: null);
} }
} }
} }

View File

@ -2,8 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic; using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Mvc.Razor namespace Microsoft.AspNetCore.Mvc.Razor
{ {
@ -12,61 +11,18 @@ namespace Microsoft.AspNetCore.Mvc.Razor
/// </summary> /// </summary>
public struct RazorPageFactoryResult public struct RazorPageFactoryResult
{ {
/// <summary>
/// Initializes a new instance of <see cref="RazorPageFactoryResult"/> with the
/// specified <paramref name="expirationTokens"/>.
/// </summary>
/// <param name="expirationTokens">One or more <see cref="IChangeToken"/> instances.</param>
public RazorPageFactoryResult(IList<IChangeToken> expirationTokens)
{
if (expirationTokens == null)
{
throw new ArgumentNullException(nameof(expirationTokens));
}
ExpirationTokens = expirationTokens;
RazorPageFactory = null;
IsPrecompiled = false;
}
/// <summary> /// <summary>
/// Initializes a new instance of <see cref="RazorPageFactoryResult"/> with the /// Initializes a new instance of <see cref="RazorPageFactoryResult"/> with the
/// specified <see cref="IRazorPage"/> factory. /// specified <see cref="IRazorPage"/> factory.
/// </summary> /// </summary>
/// <param name="razorPageFactory">The <see cref="IRazorPage"/> factory.</param> /// <param name="razorPageFactory">The <see cref="IRazorPage"/> factory.</param>
/// <param name="expirationTokens">One or more <see cref="IChangeToken"/> instances.</param> /// <param name="viewDescriptor">The <see cref="CompiledViewDescriptor"/>.</param>
public RazorPageFactoryResult( public RazorPageFactoryResult(
Func<IRazorPage> razorPageFactory, CompiledViewDescriptor viewDescriptor,
IList<IChangeToken> expirationTokens) Func<IRazorPage> razorPageFactory)
: this(razorPageFactory, expirationTokens, isPrecompiled: false)
{ {
} ViewDescriptor = viewDescriptor ?? throw new ArgumentNullException(nameof(viewDescriptor));
/// <summary>
/// Initializes a new instance of <see cref="RazorPageFactoryResult"/> with the
/// specified <see cref="IRazorPage"/> factory.
/// </summary>
/// <param name="razorPageFactory">The <see cref="IRazorPage"/> factory.</param>
/// <param name="expirationTokens">One or more <see cref="IChangeToken"/> instances.</param>
/// <param name="isPrecompiled"><c>true</c> if the view is precompiled, <c>false</c> otherwise.</param>
public RazorPageFactoryResult(
Func<IRazorPage> razorPageFactory,
IList<IChangeToken> expirationTokens,
bool isPrecompiled)
{
if (razorPageFactory == null)
{
throw new ArgumentNullException(nameof(razorPageFactory));
}
if (expirationTokens == null)
{
throw new ArgumentNullException(nameof(expirationTokens));
}
RazorPageFactory = razorPageFactory; RazorPageFactory = razorPageFactory;
ExpirationTokens = expirationTokens;
IsPrecompiled = isPrecompiled;
} }
/// <summary> /// <summary>
@ -76,19 +32,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor
public Func<IRazorPage> RazorPageFactory { get; } public Func<IRazorPage> RazorPageFactory { get; }
/// <summary> /// <summary>
/// One or more <see cref="IChangeToken"/>s associated with this instance of /// Gets the <see cref="CompiledViewDescriptor"/>.
/// <see cref="RazorPageFactoryResult"/>.
/// </summary> /// </summary>
public IList<IChangeToken> ExpirationTokens { get; } public CompiledViewDescriptor ViewDescriptor { get; }
/// <summary> /// <summary>
/// Gets a value that determines if the page was successfully located. /// Gets a value that determines if the page was successfully located.
/// </summary> /// </summary>
public bool Success => RazorPageFactory != null; public bool Success => RazorPageFactory != null;
/// <summary>
/// Gets a value that determines if the view is precompiled.
/// </summary>
public bool IsPrecompiled { get; }
} }
} }

View File

@ -396,11 +396,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor
bool isMainPage) bool isMainPage)
{ {
var factoryResult = _pageFactory.CreateFactory(relativePath); var factoryResult = _pageFactory.CreateFactory(relativePath);
if (factoryResult.ExpirationTokens != null) var viewDescriptor = factoryResult.ViewDescriptor;
if (viewDescriptor?.ExpirationTokens != null)
{ {
for (var i = 0; i < factoryResult.ExpirationTokens.Count; i++) for (var i = 0; i < viewDescriptor.ExpirationTokens.Count; i++)
{ {
expirationTokens.Add(factoryResult.ExpirationTokens[i]); expirationTokens.Add(viewDescriptor.ExpirationTokens[i]);
} }
} }
@ -408,9 +409,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor
{ {
// Only need to lookup _ViewStarts for the main page. // Only need to lookup _ViewStarts for the main page.
var viewStartPages = isMainPage ? var viewStartPages = isMainPage ?
GetViewStartPages(relativePath, expirationTokens) : GetViewStartPages(viewDescriptor.RelativePath, expirationTokens) :
Array.Empty<ViewLocationCacheItem>(); Array.Empty<ViewLocationCacheItem>();
if (factoryResult.IsPrecompiled) if (viewDescriptor.IsPrecompiled)
{ {
_logger.PrecompiledViewFound(relativePath); _logger.PrecompiledViewFound(relativePath);
} }
@ -427,17 +428,17 @@ namespace Microsoft.AspNetCore.Mvc.Razor
string path, string path,
HashSet<IChangeToken> expirationTokens) HashSet<IChangeToken> expirationTokens)
{ {
var applicationRelativePath = MakePathApplicationRelative(path);
var viewStartPages = new List<ViewLocationCacheItem>(); var viewStartPages = new List<ViewLocationCacheItem>();
foreach (var viewStartProjectItem in _razorProject.FindHierarchicalItems(applicationRelativePath, ViewStartFileName)) foreach (var viewStartProjectItem in _razorProject.FindHierarchicalItems(path, ViewStartFileName))
{ {
var result = _pageFactory.CreateFactory(viewStartProjectItem.Path); var result = _pageFactory.CreateFactory(viewStartProjectItem.Path);
if (result.ExpirationTokens != null) var viewDescriptor = result.ViewDescriptor;
if (viewDescriptor?.ExpirationTokens != null)
{ {
for (var i = 0; i < result.ExpirationTokens.Count; i++) for (var i = 0; i < viewDescriptor.ExpirationTokens.Count; i++)
{ {
expirationTokens.Add(result.ExpirationTokens[i]); expirationTokens.Add(viewDescriptor.ExpirationTokens[i]);
} }
} }
@ -479,22 +480,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor
return name[0] == '~' || name[0] == '/'; return name[0] == '~' || name[0] == '/';
} }
private string MakePathApplicationRelative(string path)
{
Debug.Assert(!string.IsNullOrEmpty(path));
if (path[0] == '~')
{
path = path.Substring(1);
}
if (path[0] != '/')
{
path = '/' + path;
}
return path;
}
private static bool IsRelativePath(string name) private static bool IsRelativePath(string name)
{ {
Debug.Assert(!string.IsNullOrEmpty(name)); Debug.Assert(!string.IsNullOrEmpty(name));

View File

@ -476,5 +476,21 @@ Partial that does not specify Layout
ignoreLineEndingDifferences: true); ignoreLineEndingDifferences: true);
#endif #endif
} }
[Fact]
public async Task ViewEngine_NormalizesPathsReturnedByViewLocationExpanders()
{
// Arrange
var expected =
@"Layout
Page
Partial";
// Act
var responseContent = await Client.GetStringAsync("/BackSlash");
// Assert
Assert.Equal(expected, responseContent.Trim());
}
} }
} }

View File

@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
public class DefaultRazorPageFactoryProviderTest public class DefaultRazorPageFactoryProviderTest
{ {
[Fact] [Fact]
public void CreateFactory_ReturnsExpirationTokensFromCompilerCache_ForUnsuccessfulResults() public void CreateFactory_ReturnsViewDescriptor_ForUnsuccessfulResults()
{ {
// Arrange // Arrange
var path = "/file-does-not-exist"; var path = "/file-does-not-exist";
@ -39,11 +39,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
// Assert // Assert
Assert.False(result.Success); Assert.False(result.Success);
Assert.Equal(expirationTokens, result.ExpirationTokens); Assert.Same(descriptor, result.ViewDescriptor);
} }
[Fact] [Fact]
public void CreateFactory_ReturnsExpirationTokensFromCompilerCache_ForSuccessfulResults() public void CreateFactory_ReturnsViewDescriptor_ForSuccessfulResults()
{ {
// Arrange // Arrange
var relativePath = "/file-exists"; var relativePath = "/file-exists";

View File

@ -8,6 +8,7 @@ using System.Threading;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Mvc.Razor.Internal; using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewEngines;
@ -169,15 +170,15 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
pageFactory pageFactory
.Setup(p => p.CreateFactory("/Views/bar/test-view.cshtml")) .Setup(p => p.CreateFactory("/Views/bar/test-view.cshtml"))
.Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])); .Returns(GetPageFactoryResult(() => page));
pageFactory pageFactory
.Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml")) .Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(() => viewStart2, new IChangeToken[0])); .Returns(GetPageFactoryResult(() => viewStart2));
pageFactory pageFactory
.Setup(p => p.CreateFactory("/_ViewStart.cshtml")) .Setup(p => p.CreateFactory("/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(() => viewStart1, new IChangeToken[0])); .Returns(GetPageFactoryResult(() => viewStart1));
var viewEngine = CreateViewEngine(pageFactory.Object); var viewEngine = CreateViewEngine(pageFactory.Object);
var context = GetActionContext(_controllerTestContext); var context = GetActionContext(_controllerTestContext);
@ -205,11 +206,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
pageFactory pageFactory
.Setup(p => p.CreateFactory("/Views/bar/test-view.cshtml")) .Setup(p => p.CreateFactory("/Views/bar/test-view.cshtml"))
.Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])); .Returns(GetPageFactoryResult(() => page));
pageFactory pageFactory
.Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml")) .Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(() => viewStart, new[] { changeToken })); .Returns(GetPageFactoryResult(() => viewStart, new[] { changeToken }));
var viewEngine = CreateViewEngine(pageFactory.Object); var viewEngine = CreateViewEngine(pageFactory.Object);
var context = GetActionContext(_controllerTestContext); var context = GetActionContext(_controllerTestContext);
@ -334,15 +335,15 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
pageFactory pageFactory
.Setup(p => p.CreateFactory("/Views/bar/test-view.cshtml")) .Setup(p => p.CreateFactory("/Views/bar/test-view.cshtml"))
.Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])); .Returns(GetPageFactoryResult(() => page));
pageFactory pageFactory
.Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml")) .Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(() => viewStart2, new IChangeToken[0])); .Returns(GetPageFactoryResult(() => viewStart2));
pageFactory pageFactory
.Setup(p => p.CreateFactory("/_ViewStart.cshtml")) .Setup(p => p.CreateFactory("/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(() => viewStart1, new IChangeToken[0])); .Returns(GetPageFactoryResult(() => viewStart1));
var viewEngine = CreateViewEngine(pageFactory.Object); var viewEngine = CreateViewEngine(pageFactory.Object);
var context = GetActionContext(_controllerTestContext); var context = GetActionContext(_controllerTestContext);
@ -370,7 +371,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
var page = Mock.Of<IRazorPage>(); var page = Mock.Of<IRazorPage>();
pageFactory pageFactory
.Setup(p => p.CreateFactory("fake-path1/bar/test-view.rzr")) .Setup(p => p.CreateFactory("fake-path1/bar/test-view.rzr"))
.Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) .Returns(GetPageFactoryResult(() => page))
.Verifiable(); .Verifiable();
var viewEngine = new TestableRazorViewEngine( var viewEngine = new TestableRazorViewEngine(
pageFactory.Object, pageFactory.Object,
@ -398,7 +399,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
var page = Mock.Of<IRazorPage>(); var page = Mock.Of<IRazorPage>();
pageFactory pageFactory
.Setup(p => p.CreateFactory(expectedViewName)) .Setup(p => p.CreateFactory(expectedViewName))
.Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) .Returns(GetPageFactoryResult(() => page))
.Verifiable(); .Verifiable();
var viewEngine = new TestableRazorViewEngine( var viewEngine = new TestableRazorViewEngine(
pageFactory.Object, pageFactory.Object,
@ -427,7 +428,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
var page = Mock.Of<IRazorPage>(); var page = Mock.Of<IRazorPage>();
pageFactory pageFactory
.Setup(p => p.CreateFactory(expectedViewName)) .Setup(p => p.CreateFactory(expectedViewName))
.Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) .Returns(GetPageFactoryResult(() => page))
.Verifiable(); .Verifiable();
var viewEngine = new TestableRazorViewEngine( var viewEngine = new TestableRazorViewEngine(
pageFactory.Object, pageFactory.Object,
@ -453,7 +454,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
var page = Mock.Of<IRazorPage>(); var page = Mock.Of<IRazorPage>();
pageFactory pageFactory
.Setup(p => p.CreateFactory(expectedViewName)) .Setup(p => p.CreateFactory(expectedViewName))
.Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) .Returns(GetPageFactoryResult(() => page))
.Verifiable(); .Verifiable();
var viewEngine = new TestableRazorViewEngine( var viewEngine = new TestableRazorViewEngine(
pageFactory.Object, pageFactory.Object,
@ -481,7 +482,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
var page = Mock.Of<IRazorPage>(); var page = Mock.Of<IRazorPage>();
pageFactory pageFactory
.Setup(p => p.CreateFactory(viewName)) .Setup(p => p.CreateFactory(viewName))
.Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) .Returns(GetPageFactoryResult(() => page))
.Verifiable(); .Verifiable();
var viewEngine = new TestableRazorViewEngine( var viewEngine = new TestableRazorViewEngine(
pageFactory.Object, pageFactory.Object,
@ -509,7 +510,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
var page = Mock.Of<IRazorPage>(); var page = Mock.Of<IRazorPage>();
pageFactory pageFactory
.Setup(p => p.CreateFactory(expectedViewName)) .Setup(p => p.CreateFactory(expectedViewName))
.Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) .Returns(GetPageFactoryResult(() => page))
.Verifiable(); .Verifiable();
var viewEngine = new TestableRazorViewEngine( var viewEngine = new TestableRazorViewEngine(
pageFactory.Object, pageFactory.Object,
@ -537,7 +538,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
var page = Mock.Of<IRazorPage>(); var page = Mock.Of<IRazorPage>();
pageFactory pageFactory
.Setup(p => p.CreateFactory(expectedViewName)) .Setup(p => p.CreateFactory(expectedViewName))
.Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) .Returns(GetPageFactoryResult(() => page))
.Verifiable(); .Verifiable();
var viewEngine = new TestableRazorViewEngine( var viewEngine = new TestableRazorViewEngine(
pageFactory.Object, pageFactory.Object,
@ -565,10 +566,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
var nonAreaPage = Mock.Of<IRazorPage>(); var nonAreaPage = Mock.Of<IRazorPage>();
pageFactory pageFactory
.Setup(p => p.CreateFactory("/Areas/Admin/Views/Home/Index.cshtml")) .Setup(p => p.CreateFactory("/Areas/Admin/Views/Home/Index.cshtml"))
.Returns(new RazorPageFactoryResult(() => areaPage, new IChangeToken[0])); .Returns(GetPageFactoryResult(() => areaPage));
pageFactory pageFactory
.Setup(p => p.CreateFactory("/Views/Home/Index.cshtml")) .Setup(p => p.CreateFactory("/Views/Home/Index.cshtml"))
.Returns(new RazorPageFactoryResult(() => nonAreaPage, new IChangeToken[0])); .Returns(GetPageFactoryResult(() => nonAreaPage));
var viewEngine = new TestableRazorViewEngine( var viewEngine = new TestableRazorViewEngine(
pageFactory.Object, pageFactory.Object,
@ -629,10 +630,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
var areaPage2 = Mock.Of<IRazorPage>(); var areaPage2 = Mock.Of<IRazorPage>();
pageFactory pageFactory
.Setup(p => p.CreateFactory("/Areas/Marketing/Views/Home/Index.cshtml")) .Setup(p => p.CreateFactory("/Areas/Marketing/Views/Home/Index.cshtml"))
.Returns(new RazorPageFactoryResult(() => areaPage1, new IChangeToken[0])); .Returns(GetPageFactoryResult(() => areaPage1));
pageFactory pageFactory
.Setup(p => p.CreateFactory("/Areas/Sales/Views/Home/Index.cshtml")) .Setup(p => p.CreateFactory("/Areas/Sales/Views/Home/Index.cshtml"))
.Returns(new RazorPageFactoryResult(() => areaPage2, new IChangeToken[0])); .Returns(GetPageFactoryResult(() => areaPage2));
var viewEngine = new TestableRazorViewEngine( var viewEngine = new TestableRazorViewEngine(
pageFactory.Object, pageFactory.Object,
@ -693,7 +694,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
var pageFactory = new Mock<IRazorPageFactoryProvider>(); var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory pageFactory
.Setup(p => p.CreateFactory("test-string/bar.cshtml")) .Setup(p => p.CreateFactory("test-string/bar.cshtml"))
.Returns(new RazorPageFactoryResult(() => Mock.Of<IRazorPage>(), new IChangeToken[0])) .Returns(GetPageFactoryResult(() => Mock.Of<IRazorPage>()))
.Verifiable(); .Verifiable();
var expander1Result = new[] { "some-seed" }; var expander1Result = new[] { "some-seed" };
@ -746,6 +747,37 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
expander2.Verify(); expander2.Verify();
} }
[Fact]
public void FindView_NoramlizesPaths_ReturnedByViewLocationExpanders()
{
// Arrange
var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory
.Setup(p => p.CreateFactory(@"Views\Home\Index.cshtml"))
.Returns(GetPageFactoryResult(() => Mock.Of<IRazorPage>()))
.Verifiable();
var expander = new Mock<IViewLocationExpander>();
expander
.Setup(e => e.ExpandViewLocations(
It.IsAny<ViewLocationExpanderContext>(),
It.IsAny<IEnumerable<string>>()))
.Returns(new[] { @"Views\Home\Index.cshtml" });
var viewEngine = CreateViewEngine(
pageFactory.Object,
new[] { expander.Object });
var context = GetActionContext(new Dictionary<string, object>());
// Act
var result = viewEngine.FindView(context, "test-view", isMainPage: true);
// Assert
Assert.True(result.Success);
Assert.IsAssignableFrom<IView>(result.View);
pageFactory.Verify();
}
[Fact] [Fact]
public void FindView_CachesValuesIfViewWasFound() public void FindView_CachesValuesIfViewWasFound()
{ {
@ -754,11 +786,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
var pageFactory = new Mock<IRazorPageFactoryProvider>(); var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory pageFactory
.Setup(p => p.CreateFactory("/Views/bar/baz.cshtml")) .Setup(p => p.CreateFactory("/Views/bar/baz.cshtml"))
.Returns(new RazorPageFactoryResult(new IChangeToken[0])) .Returns(GetPageFactoryResult(factory: null))
.Verifiable(); .Verifiable();
pageFactory pageFactory
.Setup(p => p.CreateFactory("/Views/Shared/baz.cshtml")) .Setup(p => p.CreateFactory("/Views/Shared/baz.cshtml"))
.Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) .Returns(GetPageFactoryResult(() => page))
.Verifiable(); .Verifiable();
var viewEngine = CreateViewEngine(pageFactory.Object); var viewEngine = CreateViewEngine(pageFactory.Object);
@ -795,7 +827,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
var pageFactory = new Mock<IRazorPageFactoryProvider>(); var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory pageFactory
.Setup(p => p.CreateFactory("/Views/Shared/baz.cshtml")) .Setup(p => p.CreateFactory("/Views/Shared/baz.cshtml"))
.Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) .Returns(GetPageFactoryResult(() => page))
.Verifiable(); .Verifiable();
var viewEngine = CreateViewEngine(pageFactory.Object); var viewEngine = CreateViewEngine(pageFactory.Object);
@ -838,16 +870,16 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
pageFactory pageFactory
.InSequence(sequence) .InSequence(sequence)
.Setup(p => p.CreateFactory("/Views/bar/baz.cshtml")) .Setup(p => p.CreateFactory("/Views/bar/baz.cshtml"))
.Returns(new RazorPageFactoryResult(new[] { changeToken })); .Returns(GetPageFactoryResult(factory: null, changeTokens: new[] { changeToken }));
pageFactory pageFactory
.InSequence(sequence) .InSequence(sequence)
.Setup(p => p.CreateFactory("/Views/Shared/baz.cshtml")) .Setup(p => p.CreateFactory("/Views/Shared/baz.cshtml"))
.Returns(new RazorPageFactoryResult(() => page1, new IChangeToken[0])) .Returns(GetPageFactoryResult(() => page1))
.Verifiable(); .Verifiable();
pageFactory pageFactory
.InSequence(sequence) .InSequence(sequence)
.Setup(p => p.CreateFactory("/Views/bar/baz.cshtml")) .Setup(p => p.CreateFactory("/Views/bar/baz.cshtml"))
.Returns(new RazorPageFactoryResult(() => page2, new IChangeToken[0])); .Returns(GetPageFactoryResult(() => page2));
var viewEngine = CreateViewEngine(pageFactory.Object); var viewEngine = CreateViewEngine(pageFactory.Object);
var context = GetActionContext(_controllerTestContext); var context = GetActionContext(_controllerTestContext);
@ -886,20 +918,20 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
pageFactory pageFactory
.InSequence(sequence) .InSequence(sequence)
.Setup(p => p.CreateFactory("/Views/bar/baz.cshtml")) .Setup(p => p.CreateFactory("/Views/bar/baz.cshtml"))
.Returns(new RazorPageFactoryResult(() => page1, new IChangeToken[0])); .Returns(GetPageFactoryResult(() => page1));
pageFactory pageFactory
.InSequence(sequence) .InSequence(sequence)
.Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml")) .Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(new[] { changeToken })) .Returns(GetPageFactoryResult(factory: null, changeTokens: new[] { changeToken }))
.Verifiable(); .Verifiable();
pageFactory pageFactory
.InSequence(sequence) .InSequence(sequence)
.Setup(p => p.CreateFactory("/Views/bar/baz.cshtml")) .Setup(p => p.CreateFactory("/Views/bar/baz.cshtml"))
.Returns(new RazorPageFactoryResult(() => page2, new IChangeToken[0])); .Returns(GetPageFactoryResult(() => page2));
pageFactory pageFactory
.InSequence(sequence) .InSequence(sequence)
.Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml")) .Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(() => viewStart, new IChangeToken[0])); .Returns(GetPageFactoryResult(() => viewStart));
var fileProvider = new TestFileProvider(); var fileProvider = new TestFileProvider();
var razorProject = new FileProviderRazorProject(fileProvider); var razorProject = new FileProviderRazorProject(fileProvider);
@ -993,7 +1025,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
var pageFactory = new Mock<IRazorPageFactoryProvider>(); var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory pageFactory
.Setup(p => p.CreateFactory("viewlocation3")) .Setup(p => p.CreateFactory("viewlocation3"))
.Returns(new RazorPageFactoryResult(new[] { changeToken })); .Returns(GetPageFactoryResult(factory: null, changeTokens: new[] { changeToken }));
var expander = new Mock<IViewLocationExpander>(); var expander = new Mock<IViewLocationExpander>();
var expandedLocations = new[] var expandedLocations = new[]
{ {
@ -1031,7 +1063,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
// Act - 2 // Act - 2
pageFactory pageFactory
.Setup(p => p.CreateFactory("viewlocation3")) .Setup(p => p.CreateFactory("viewlocation3"))
.Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])); .Returns(GetPageFactoryResult(() => page));
cancellationTokenSource.Cancel(); cancellationTokenSource.Cancel();
result = viewEngine.FindView(context, "MyView", isMainPage: true); result = viewEngine.FindView(context, "MyView", isMainPage: true);
@ -1131,7 +1163,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
var pageFactory = new Mock<IRazorPageFactoryProvider>(); var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory pageFactory
.Setup(p => p.CreateFactory("expanded-path/bar-layout")) .Setup(p => p.CreateFactory("expanded-path/bar-layout"))
.Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) .Returns(GetPageFactoryResult(() => page))
.Verifiable(); .Verifiable();
var expander = new Mock<IViewLocationExpander>(); var expander = new Mock<IViewLocationExpander>();
@ -1210,7 +1242,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
var pageFactory = new Mock<IRazorPageFactoryProvider>(); var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory pageFactory
.Setup(p => p.CreateFactory("/Views/Foo/details.cshtml")) .Setup(p => p.CreateFactory("/Views/Foo/details.cshtml"))
.Returns(new RazorPageFactoryResult(() => page.Object, new IChangeToken[0])) .Returns(GetPageFactoryResult(() => page.Object))
.Verifiable(); .Verifiable();
var viewEngine = CreateViewEngine(pageFactory.Object); var viewEngine = CreateViewEngine(pageFactory.Object);
@ -1339,10 +1371,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
var loggerFactory = new TestLoggerFactory(sink, enabled: true); var loggerFactory = new TestLoggerFactory(sink, enabled: true);
var relativePath = "/Views/Foo/details.cshtml"; var relativePath = "/Views/Foo/details.cshtml";
var factoryResult = GetPageFactoryResult(() => Mock.Of<IRazorPage>());
factoryResult.ViewDescriptor.IsPrecompiled = true;
var pageFactory = new Mock<IRazorPageFactoryProvider>(); var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory pageFactory
.Setup(p => p.CreateFactory(relativePath)) .Setup(p => p.CreateFactory(relativePath))
.Returns(new RazorPageFactoryResult(() => Mock.Of<IRazorPage>(), new IChangeToken[0], isPrecompiled: true)) .Returns(factoryResult)
.Verifiable(); .Verifiable();
var viewEngine = new RazorViewEngine( var viewEngine = new RazorViewEngine(
@ -1375,7 +1409,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
var page = Mock.Of<IRazorPage>(); var page = Mock.Of<IRazorPage>();
pageFactory pageFactory
.Setup(p => p.CreateFactory(pageName)) .Setup(p => p.CreateFactory(pageName))
.Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) .Returns(GetPageFactoryResult(() => page))
.Verifiable(); .Verifiable();
var viewEngine = new TestableRazorViewEngine( var viewEngine = new TestableRazorViewEngine(
pageFactory.Object, pageFactory.Object,
@ -1403,7 +1437,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
var page = Mock.Of<IRazorPage>(); var page = Mock.Of<IRazorPage>();
pageFactory pageFactory
.Setup(p => p.CreateFactory(expectedPageName)) .Setup(p => p.CreateFactory(expectedPageName))
.Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) .Returns(GetPageFactoryResult(() => page))
.Verifiable(); .Verifiable();
var viewEngine = new TestableRazorViewEngine( var viewEngine = new TestableRazorViewEngine(
pageFactory.Object, pageFactory.Object,
@ -1431,7 +1465,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
var page = Mock.Of<IRazorPage>(); var page = Mock.Of<IRazorPage>();
pageFactory pageFactory
.Setup(p => p.CreateFactory(expectedPageName)) .Setup(p => p.CreateFactory(expectedPageName))
.Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) .Returns(GetPageFactoryResult(() => page))
.Verifiable(); .Verifiable();
var viewEngine = new TestableRazorViewEngine( var viewEngine = new TestableRazorViewEngine(
pageFactory.Object, pageFactory.Object,
@ -1761,11 +1795,25 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
var pageFactory = new Mock<IRazorPageFactoryProvider>(MockBehavior.Strict); var pageFactory = new Mock<IRazorPageFactoryProvider>(MockBehavior.Strict);
pageFactory pageFactory
.Setup(f => f.CreateFactory(It.IsAny<string>())) .Setup(f => f.CreateFactory(It.IsAny<string>()))
.Returns(new RazorPageFactoryResult(() => Mock.Of<IRazorPage>(), new IChangeToken[0])); .Returns(GetPageFactoryResult(() => Mock.Of<IRazorPage>()));
return CreateViewEngine(pageFactory.Object); return CreateViewEngine(pageFactory.Object);
} }
private static RazorPageFactoryResult GetPageFactoryResult(
Func<IRazorPage> factory,
IList<IChangeToken> changeTokens = null,
string path = "/Views/Home/Index.cshtml")
{
var descriptor = new CompiledViewDescriptor
{
ExpirationTokens = changeTokens ?? Array.Empty<IChangeToken>(),
RelativePath = path,
};
return new RazorPageFactoryResult(descriptor, factory);
}
private TestableRazorViewEngine CreateViewEngine( private TestableRazorViewEngine CreateViewEngine(
IRazorPageFactoryProvider pageFactory = null, IRazorPageFactoryProvider pageFactory = null,
IEnumerable<IViewLocationExpander> expanders = null, IEnumerable<IViewLocationExpander> expanders = null,

View File

@ -10,6 +10,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
@ -255,10 +256,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor
v.Write("layout-content" + Environment.NewLine); v.Write("layout-content" + Environment.NewLine);
v.RenderBodyPublic(); v.RenderBodyPublic();
}); });
var pageFactoryResult = new RazorPageFactoryResult(new CompiledViewDescriptor(), () => layout);
var pageFactory = new Mock<IRazorPageFactoryProvider>(); var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory pageFactory
.Setup(p => p.CreateFactory(LayoutPath)) .Setup(p => p.CreateFactory(LayoutPath))
.Returns(new RazorPageFactoryResult(() => layout, new IChangeToken[0])); .Returns(pageFactoryResult);
var viewEngine = new Mock<IRazorViewEngine>(MockBehavior.Strict); var viewEngine = new Mock<IRazorViewEngine>(MockBehavior.Strict);
viewEngine viewEngine

View File

@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Mvc.Razor.Internal; using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
@ -181,10 +182,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
razorPageFactoryProvider razorPageFactoryProvider
.Setup(f => f.CreateFactory("/Home/Path1/_ViewStart.cshtml")) .Setup(f => f.CreateFactory("/Home/Path1/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(factory1, new IChangeToken[0])); .Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), factory1));
razorPageFactoryProvider razorPageFactoryProvider
.Setup(f => f.CreateFactory("/_ViewStart.cshtml")) .Setup(f => f.CreateFactory("/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(factory2, new[] { Mock.Of<IChangeToken>() })); .Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), factory2));
var fileProvider = new TestFileProvider(); var fileProvider = new TestFileProvider();
fileProvider.AddFile("/Home/Path1/_ViewStart.cshtml", "content1"); fileProvider.AddFile("/Home/Path1/_ViewStart.cshtml", "content1");
@ -215,7 +216,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
Assert.Equal(new[] { factory2, factory1 }, entry.ViewStartFactories); Assert.Equal(new[] { factory2, factory1 }, entry.ViewStartFactories);
} }
[Fact] [Fact]
public void OnProvidersExecuting_CachesEntries() public void OnProvidersExecuting_CachesEntries()
{ {
@ -347,19 +347,19 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
var mock = new Mock<IRazorPageFactoryProvider>(MockBehavior.Strict); var mock = new Mock<IRazorPageFactoryProvider>(MockBehavior.Strict);
mock mock
.Setup(p => p.CreateFactory("/Pages/Level1/Level2/_ViewStart.cshtml")) .Setup(p => p.CreateFactory("/Pages/Level1/Level2/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(() => null, new List<IChangeToken>())) .Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), () => null))
.Verifiable(); .Verifiable();
mock mock
.Setup(p => p.CreateFactory("/Pages/Level1/_ViewStart.cshtml")) .Setup(p => p.CreateFactory("/Pages/Level1/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(() => null, new List<IChangeToken>())) .Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), () => null))
.Verifiable(); .Verifiable();
mock mock
.Setup(p => p.CreateFactory("/Pages/_ViewStart.cshtml")) .Setup(p => p.CreateFactory("/Pages/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(() => null, new List<IChangeToken>())) .Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), () => null))
.Verifiable(); .Verifiable();
mock mock
.Setup(p => p.CreateFactory("/_ViewStart.cshtml")) .Setup(p => p.CreateFactory("/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(() => null, new List<IChangeToken>())) .Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), () => null))
.Verifiable(); .Verifiable();
var razorPageFactoryProvider = mock.Object; var razorPageFactoryProvider = mock.Object;
@ -398,13 +398,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
var pageFactory = new Mock<IRazorPageFactoryProvider>(); var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory pageFactory
.Setup(f => f.CreateFactory("/Views/Deeper/_ViewStart.cshtml")) .Setup(f => f.CreateFactory("/Views/Deeper/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(() => null, new IChangeToken[0])); .Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), () => null));
pageFactory pageFactory
.Setup(f => f.CreateFactory("/Views/_ViewStart.cshtml")) .Setup(f => f.CreateFactory("/Views/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(new IChangeToken[0])); .Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), razorPageFactory: null));
pageFactory pageFactory
.Setup(f => f.CreateFactory("/_ViewStart.cshtml")) .Setup(f => f.CreateFactory("/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(() => null, new IChangeToken[0])); .Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), () => null));
// No files // No files
var fileProvider = new TestFileProvider(); var fileProvider = new TestFileProvider();

View File

@ -0,0 +1,12 @@
// 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 Microsoft.AspNetCore.Mvc;
namespace RazorWebSite.Controllers
{
public class BackSlashController : Controller
{
public IActionResult Index() => View(@"Views\BackSlash\BackSlashView.cshtml");
}
}

View File

@ -19,5 +19,6 @@
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(AspNetCoreVersion)" /> <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="$(AspNetCoreVersion)" /> <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="$(AspNetCoreVersion)" /> <PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="$(AspNetCoreVersion)" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,27 @@
// 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.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace RazorWebSite
{
public class ForwardSlashExpander : IViewLocationExpander
{
public void PopulateValues(ViewLocationExpanderContext context)
{
}
public virtual IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
if (context.ActionContext is ViewContext viewContext && (string)viewContext.ViewData["back-slash"] == "true")
{
return new[] { $@"Views\BackSlash\{context.ViewName}.cshtml" };
}
return viewLocations;
}
}
}

View File

@ -33,6 +33,7 @@ namespace RazorWebSite
$"{nameof(RazorWebSite)}.EmbeddedViews")); $"{nameof(RazorWebSite)}.EmbeddedViews"));
options.FileProviders.Add(updateableFileProvider); options.FileProviders.Add(updateableFileProvider);
options.ViewLocationExpanders.Add(new NonMainPageViewLocationExpander()); options.ViewLocationExpanders.Add(new NonMainPageViewLocationExpander());
options.ViewLocationExpanders.Add(new ForwardSlashExpander());
}) })
.AddViewOptions(options => .AddViewOptions(options =>
{ {
@ -51,6 +52,7 @@ namespace RazorWebSite
public void Configure(IApplicationBuilder app) public void Configure(IApplicationBuilder app)
{ {
app.UseDeveloperExceptionPage();
app.UseRequestLocalization(new RequestLocalizationOptions app.UseRequestLocalization(new RequestLocalizationOptions
{ {
DefaultRequestCulture = new RequestCulture("en-GB", "en-US"), DefaultRequestCulture = new RequestCulture("en-GB", "en-US"),

View File

@ -0,0 +1,6 @@
@{
ViewData["back-slash"] = "true";
Layout = "_Layout";
}
Page
@Html.Partial("_BackSlashPartial")

View File

@ -0,0 +1 @@
Partial

View File

@ -0,0 +1,2 @@
Layout
@RenderBody()