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
{
/// <summary>
/// The normalized application relative path of the view.
/// </summary>
public string RelativePath { get; set; }
/// <summary>

View File

@ -57,11 +57,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
var pageFactory = Expression
.Lambda<Func<IRazorPage>>(objectInitializeExpression)
.Compile();
return new RazorPageFactoryResult(pageFactory, viewDescriptor.ExpirationTokens, viewDescriptor.IsPrecompiled);
return new RazorPageFactoryResult(viewDescriptor, pageFactory);
}
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.
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Primitives;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
namespace Microsoft.AspNetCore.Mvc.Razor
{
@ -12,61 +11,18 @@ namespace Microsoft.AspNetCore.Mvc.Razor
/// </summary>
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>
/// 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="viewDescriptor">The <see cref="CompiledViewDescriptor"/>.</param>
public RazorPageFactoryResult(
Func<IRazorPage> razorPageFactory,
IList<IChangeToken> expirationTokens)
: this(razorPageFactory, expirationTokens, isPrecompiled: false)
CompiledViewDescriptor viewDescriptor,
Func<IRazorPage> razorPageFactory)
{
}
/// <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));
}
ViewDescriptor = viewDescriptor ?? throw new ArgumentNullException(nameof(viewDescriptor));
RazorPageFactory = razorPageFactory;
ExpirationTokens = expirationTokens;
IsPrecompiled = isPrecompiled;
}
/// <summary>
@ -76,19 +32,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor
public Func<IRazorPage> RazorPageFactory { get; }
/// <summary>
/// One or more <see cref="IChangeToken"/>s associated with this instance of
/// <see cref="RazorPageFactoryResult"/>.
/// Gets the <see cref="CompiledViewDescriptor"/>.
/// </summary>
public IList<IChangeToken> ExpirationTokens { get; }
public CompiledViewDescriptor ViewDescriptor { get; }
/// <summary>
/// Gets a value that determines if the page was successfully located.
/// </summary>
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)
{
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.
var viewStartPages = isMainPage ?
GetViewStartPages(relativePath, expirationTokens) :
GetViewStartPages(viewDescriptor.RelativePath, expirationTokens) :
Array.Empty<ViewLocationCacheItem>();
if (factoryResult.IsPrecompiled)
if (viewDescriptor.IsPrecompiled)
{
_logger.PrecompiledViewFound(relativePath);
}
@ -427,17 +428,17 @@ namespace Microsoft.AspNetCore.Mvc.Razor
string path,
HashSet<IChangeToken> expirationTokens)
{
var applicationRelativePath = MakePathApplicationRelative(path);
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);
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] == '/';
}
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)
{
Debug.Assert(!string.IsNullOrEmpty(name));

View File

@ -476,5 +476,21 @@ Partial that does not specify Layout
ignoreLineEndingDifferences: true);
#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
{
[Fact]
public void CreateFactory_ReturnsExpirationTokensFromCompilerCache_ForUnsuccessfulResults()
public void CreateFactory_ReturnsViewDescriptor_ForUnsuccessfulResults()
{
// Arrange
var path = "/file-does-not-exist";
@ -39,11 +39,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
// Assert
Assert.False(result.Success);
Assert.Equal(expirationTokens, result.ExpirationTokens);
Assert.Same(descriptor, result.ViewDescriptor);
}
[Fact]
public void CreateFactory_ReturnsExpirationTokensFromCompilerCache_ForSuccessfulResults()
public void CreateFactory_ReturnsViewDescriptor_ForSuccessfulResults()
{
// Arrange
var relativePath = "/file-exists";

View File

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

View File

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

View File

@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.AspNetCore.Mvc.Rendering;
@ -181,10 +182,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
razorPageFactoryProvider
.Setup(f => f.CreateFactory("/Home/Path1/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(factory1, new IChangeToken[0]));
.Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), factory1));
razorPageFactoryProvider
.Setup(f => f.CreateFactory("/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(factory2, new[] { Mock.Of<IChangeToken>() }));
.Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), factory2));
var fileProvider = new TestFileProvider();
fileProvider.AddFile("/Home/Path1/_ViewStart.cshtml", "content1");
@ -215,7 +216,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
Assert.Equal(new[] { factory2, factory1 }, entry.ViewStartFactories);
}
[Fact]
public void OnProvidersExecuting_CachesEntries()
{
@ -347,19 +347,19 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
var mock = new Mock<IRazorPageFactoryProvider>(MockBehavior.Strict);
mock
.Setup(p => p.CreateFactory("/Pages/Level1/Level2/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(() => null, new List<IChangeToken>()))
.Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), () => null))
.Verifiable();
mock
.Setup(p => p.CreateFactory("/Pages/Level1/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(() => null, new List<IChangeToken>()))
.Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), () => null))
.Verifiable();
mock
.Setup(p => p.CreateFactory("/Pages/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(() => null, new List<IChangeToken>()))
.Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), () => null))
.Verifiable();
mock
.Setup(p => p.CreateFactory("/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(() => null, new List<IChangeToken>()))
.Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), () => null))
.Verifiable();
var razorPageFactoryProvider = mock.Object;
@ -398,13 +398,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory
.Setup(f => f.CreateFactory("/Views/Deeper/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(() => null, new IChangeToken[0]));
.Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), () => null));
pageFactory
.Setup(f => f.CreateFactory("/Views/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(new IChangeToken[0]));
.Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), razorPageFactory: null));
pageFactory
.Setup(f => f.CreateFactory("/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(() => null, new IChangeToken[0]));
.Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), () => null));
// No files
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.StaticFiles" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="$(AspNetCoreVersion)" />
</ItemGroup>
</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"));
options.FileProviders.Add(updateableFileProvider);
options.ViewLocationExpanders.Add(new NonMainPageViewLocationExpander());
options.ViewLocationExpanders.Add(new ForwardSlashExpander());
})
.AddViewOptions(options =>
{
@ -51,6 +52,7 @@ namespace RazorWebSite
public void Configure(IApplicationBuilder app)
{
app.UseDeveloperExceptionPage();
app.UseRequestLocalization(new RequestLocalizationOptions
{
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()