diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs index e1c2a734e6..8c460c1ceb 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs @@ -462,6 +462,22 @@ namespace Microsoft.AspNetCore.Mvc.Razor return string.Format(CultureInfo.CurrentCulture, GetString("Compilation_DependencyContextIsNotSpecified"), p0, p1, p2); } + /// + /// '{0}' cannot be empty. These locations are required to locate a view for rendering. + /// + internal static string ViewLocationFormatsIsRequired + { + get { return GetString("ViewLocationFormatsIsRequired"); } + } + + /// + /// '{0}' cannot be empty. These locations are required to locate a view for rendering. + /// + internal static string FormatViewLocationFormatsIsRequired(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ViewLocationFormatsIsRequired"), p0); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngine.cs b/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngine.cs index 09a333451d..3bce3cf339 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngine.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngine.cs @@ -8,7 +8,6 @@ using System.Globalization; using System.Linq; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Mvc.Razor.Internal; -using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; @@ -21,12 +20,15 @@ namespace Microsoft.AspNetCore.Mvc.Razor /// Default implementation of . /// /// - /// For ViewResults returned from controllers, views should be located in - /// by default. For the controllers in an area, views should exist in . + /// For ViewResults returned from controllers, views should be located in + /// + /// by default. For the controllers in an area, views should exist in + /// . /// public class RazorViewEngine : IRazorViewEngine { - private const string ViewExtension = ".cshtml"; + public static readonly string ViewExtension = ".cshtml"; + private const string ControllerKey = "controller"; private const string AreaKey = "area"; private static readonly ViewLocationCacheItem[] EmptyViewStartLocationCacheItems = @@ -34,10 +36,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor private static readonly TimeSpan _cacheExpirationDuration = TimeSpan.FromMinutes(20); private readonly IRazorPageFactoryProvider _pageFactory; - private readonly IList _viewLocationExpanders; private readonly IRazorPageActivator _pageActivator; private readonly HtmlEncoder _htmlEncoder; private readonly ILogger _logger; + private readonly RazorViewEngineOptions _options; /// /// Initializes a new instance of the . @@ -49,9 +51,24 @@ namespace Microsoft.AspNetCore.Mvc.Razor IOptions optionsAccessor, ILoggerFactory loggerFactory) { + _options = optionsAccessor.Value; + + if (_options.ViewLocationFormats.Count == 0) + { + throw new ArgumentException( + Resources.FormatViewLocationFormatsIsRequired(nameof(RazorViewEngineOptions.ViewLocationFormats)), + nameof(optionsAccessor)); + } + + if (_options.AreaViewLocationFormats.Count == 0) + { + throw new ArgumentException( + Resources.FormatViewLocationFormatsIsRequired(nameof(RazorViewEngineOptions.AreaViewLocationFormats)), + nameof(optionsAccessor)); + } + _pageFactory = pageFactory; _pageActivator = pageActivator; - _viewLocationExpanders = optionsAccessor.Value.ViewLocationExpanders; _htmlEncoder = htmlEncoder; _logger = loggerFactory.CreateLogger(); ViewLookupCache = new MemoryCache(new MemoryCacheOptions @@ -60,47 +77,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor }); } - /// - /// Gets the locations where this instance of will search for views. - /// - /// - /// The locations of the views returned from controllers that do not belong to an area. - /// Locations are composite format strings (see http://msdn.microsoft.com/en-us/library/txafckwd.aspx), - /// which contains following indexes: - /// {0} - Action Name - /// {1} - Controller Name - /// The values for these locations are case-sensitive on case-sensitive file systems. - /// For example, the view for the Test action of HomeController should be located at - /// /Views/Home/Test.cshtml. Locations such as /views/home/test.cshtml would not be discovered - /// - public virtual IEnumerable ViewLocationFormats { get; } = new[] - { - "/Views/{1}/{0}" + ViewExtension, - "/Views/Shared/{0}" + ViewExtension, - }; - - /// - /// Gets the locations where this instance of will search for views within an - /// area. - /// - /// - /// The locations of the views returned from controllers that belong to an area. - /// Locations are composite format strings (see http://msdn.microsoft.com/en-us/library/txafckwd.aspx), - /// which contains following indexes: - /// {0} - Action Name - /// {1} - Controller Name - /// {2} - Area name - /// The values for these locations are case-sensitive on case-sensitive file systems. - /// For example, the view for the Test action of HomeController should be located at - /// /Views/Home/Test.cshtml. Locations such as /views/home/test.cshtml would not be discovered - /// - public virtual IEnumerable AreaViewLocationFormats { get; } = new[] - { - "/Areas/{2}/Views/{1}/{0}" + ViewExtension, - "/Areas/{2}/Views/Shared/{0}" + ViewExtension, - "/Views/Shared/{0}" + ViewExtension, - }; - /// /// A cache for results of view lookups. /// @@ -311,15 +287,15 @@ namespace Microsoft.AspNetCore.Mvc.Razor isMainPage); Dictionary expanderValues = null; - if (_viewLocationExpanders.Count > 0) + if (_options.ViewLocationExpanders.Count > 0) { expanderValues = new Dictionary(StringComparer.Ordinal); expanderContext.Values = expanderValues; // Perf: Avoid allocations - for (var i = 0; i < _viewLocationExpanders.Count; i++) + for (var i = 0; i < _options.ViewLocationExpanders.Count; i++) { - _viewLocationExpanders[i].PopulateValues(expanderContext); + _options.ViewLocationExpanders[i].PopulateValues(expanderContext); } } @@ -385,13 +361,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor ViewLocationCacheKey cacheKey) { // Only use the area view location formats if we have an area token. - var viewLocations = !string.IsNullOrEmpty(expanderContext.AreaName) ? - AreaViewLocationFormats : - ViewLocationFormats; + IEnumerable viewLocations = !string.IsNullOrEmpty(expanderContext.AreaName) ? + _options.AreaViewLocationFormats : + _options.ViewLocationFormats; - for (var i = 0; i < _viewLocationExpanders.Count; i++) + for (var i = 0; i < _options.ViewLocationExpanders.Count; i++) { - viewLocations = _viewLocationExpanders[i].ExpandViewLocations(expanderContext, viewLocations); + viewLocations = _options.ViewLocationExpanders[i].ExpandViewLocations(expanderContext, viewLocations); } ViewLocationCacheResult cacheResult = null; diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngineOptions.cs b/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngineOptions.cs index f2ab1f0c43..e279c88ec9 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngineOptions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngineOptions.cs @@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor private Action _compilationCallback = c => { }; /// - /// Get a used by the . + /// Gets a used by the . /// public IList ViewLocationExpanders { get; } = new List(); @@ -35,6 +35,61 @@ namespace Microsoft.AspNetCore.Mvc.Razor /// public IList FileProviders { get; } = new List(); + /// + /// Gets the locations where will search for views. + /// + /// + /// + /// The locations of the views returned from controllers that do not belong to an area. + /// Locations are composite format strings (see http://msdn.microsoft.com/en-us/library/txafckwd.aspx), + /// which may contain the following format items: + /// + /// + /// + /// {0} - Action Name + /// + /// + /// {1} - Controller Name + /// + /// + /// + /// The values for these locations are case-sensitive on case-sensitive file systems. + /// For example, the view for the Test action of HomeController should be located at + /// /Views/Home/Test.cshtml. Locations such as /views/home/test.cshtml would not be discovered. + /// + /// + public IList ViewLocationFormats { get; } = new List(); + + /// + /// Gets the locations where will search for views within an + /// area. + /// + /// + /// + /// The locations of the views returned from controllers that belong to an area. + /// Locations are composite format strings (see http://msdn.microsoft.com/en-us/library/txafckwd.aspx), + /// which may contain the following format items: + /// + /// + /// + /// {0} - Action Name + /// + /// + /// {1} - Controller Name + /// + /// + /// {2} - Area Name + /// + /// + /// + /// The values for these locations are case-sensitive on case-sensitive file systems. + /// For example, the view for the Test action of HomeController under Admin area should + /// be located at /Areas/Admin/Views/Home/Test.cshtml. + /// Locations such as /areas/admin/views/home/test.cshtml would not be discovered. + /// + /// + public IList AreaViewLocationFormats { get; } = new List(); + /// /// Gets or sets the callback that is used to customize Razor compilation /// to change compilation settings you can update property. diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngineOptionsSetup.cs b/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngineOptionsSetup.cs index 97607bc9ec..dbba63a6c3 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngineOptionsSetup.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngineOptionsSetup.cs @@ -50,6 +50,13 @@ namespace Microsoft.AspNetCore.Mvc var parseOptions = razorOptions.ParseOptions; razorOptions.ParseOptions = parseOptions.WithPreprocessorSymbols( parseOptions.PreprocessorSymbolNames.Concat(new[] { configurationSymbol })); + + razorOptions.ViewLocationFormats.Add("/Views/{1}/{0}" + RazorViewEngine.ViewExtension); + razorOptions.ViewLocationFormats.Add("/Views/Shared/{0}" + RazorViewEngine.ViewExtension); + + razorOptions.AreaViewLocationFormats.Add("/Areas/{2}/Views/{1}/{0}" + RazorViewEngine.ViewExtension); + razorOptions.AreaViewLocationFormats.Add("/Areas/{2}/Views/Shared/{0}" + RazorViewEngine.ViewExtension); + razorOptions.AreaViewLocationFormats.Add("/Views/Shared/{0}" + RazorViewEngine.ViewExtension); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx b/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx index f44490e3db..762d5356b5 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx @@ -203,4 +203,7 @@ The Razor page '{0}' failed to compile. Ensure that your application's {1} sets the '{2}' compilation property. + + '{0}' cannot be empty. These locations are required to locate a view for rendering. + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineOptionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineOptionsTest.cs index bd6b59f1ca..6172036b6c 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineOptionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineOptionsTest.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; +using System.Linq; using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.Extensions.DependencyInjection; @@ -11,6 +13,67 @@ namespace Microsoft.AspNetCore.Mvc.Razor { public class RazorViewEngineOptionsTest { + [Fact] + public void AreaViewLocationFormats_ContainsExpectedLocations() + { + // Arrange + var services = new ServiceCollection().AddOptions(); + var areaViewLocations = new[] + { + "/Areas/{2}/MvcViews/{1}/{0}.cshtml", + "/Areas/{2}/MvcViews/Shared/{0}.cshtml", + "/MvcViews/Shared/{0}.cshtml" + }; + var builder = new MvcBuilder(services, new ApplicationPartManager()); + builder.AddRazorOptions(options => + { + options.AreaViewLocationFormats.Clear(); + + foreach (var location in areaViewLocations) + { + options.AreaViewLocationFormats.Add(location); + } + }); + var serviceProvider = services.BuildServiceProvider(); + var accessor = serviceProvider.GetRequiredService>(); + + // Act + var formats = accessor.Value.AreaViewLocationFormats; + + // Assert + Assert.Equal(areaViewLocations, formats, StringComparer.Ordinal); + } + + [Fact] + public void ViewLocationFormats_ContainsExpectedLocations() + { + // Arrange + var services = new ServiceCollection().AddOptions(); + var viewLocations = new[] + { + "/MvcViews/{1}/{0}.cshtml", + "/MvcViews/Shared/{0}.cshtml" + }; + var builder = new MvcBuilder(services, new ApplicationPartManager()); + builder.AddRazorOptions(options => + { + options.ViewLocationFormats.Clear(); + + foreach (var location in viewLocations) + { + options.ViewLocationFormats.Add(location); + } + }); + var serviceProvider = services.BuildServiceProvider(); + var accessor = serviceProvider.GetRequiredService>(); + + // Act + var formats = accessor.Value.ViewLocationFormats; + + // Assert + Assert.Equal(viewLocations, formats, StringComparer.Ordinal); + } + [Fact] public void AddRazorOptions_ConfiguresOptionsAsExpected() { diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineTest.cs index 13888ae2de..5be204df1c 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineTest.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Threading; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Routing; @@ -364,10 +365,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test .Verifiable(); var viewEngine = new TestableRazorViewEngine( pageFactory.Object, - GetOptionsAccessor()); - viewEngine.SetLocationFormats( - new[] { "fake-path1/{1}/{0}.rzr" }, - new[] { "fake-area-path/{2}/{1}/{0}.rzr" }); + GetOptionsAccessor( + viewLocationFormats: new[] { "fake-path1/{1}/{0}.rzr" }, + areaViewLocationFormats: new[] { "fake-area-path/{2}/{1}/{0}.rzr" })); var context = GetActionContext(_controllerTestContext); // Act @@ -393,10 +393,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test .Verifiable(); var viewEngine = new TestableRazorViewEngine( pageFactory.Object, - GetOptionsAccessor()); - viewEngine.SetLocationFormats( - new[] { "fake-path1/{1}/{0}.rzr" }, - new[] { "fake-area-path/{2}/{1}/{0}.rzr" }); + GetOptionsAccessor( + viewLocationFormats: new[] { "fake-path1/{1}/{0}.rzr" }, + areaViewLocationFormats: new[] { "fake-area-path/{2}/{1}/{0}.rzr" })); var context = GetActionContext(_areaTestContext); // Act @@ -1462,38 +1461,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test Assert.Equal(expectedPagePath, result); } - [Fact] - public void AreaViewLocationFormats_ContainsExpectedLocations() - { - // Arrange - var viewEngine = CreateViewEngine(); - var areaViewLocations = new string[] - { - "/Areas/{2}/Views/{1}/{0}.cshtml", - "/Areas/{2}/Views/Shared/{0}.cshtml", - "/Views/Shared/{0}.cshtml" - }; - - // Act & Assert - Assert.Equal(areaViewLocations, viewEngine.AreaViewLocationFormats); - } - - [Fact] - public void ViewLocationFormats_ContainsExpectedLocations() - { - // Arrange - var viewEngine = CreateViewEngine(); - - var viewLocations = new string[] - { - "/Views/{1}/{0}.cshtml", - "/Views/Shared/{0}.cshtml" - }; - - // Act & Assert - Assert.Equal(viewLocations, viewEngine.ViewLocationFormats); - } - [Fact] public void GetNormalizedRouteValue_ReturnsValueFromRouteValues_IfKeyHandlingIsRequired() { @@ -1713,15 +1680,19 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test IEnumerable expanders = null) { pageFactory = pageFactory ?? Mock.Of(); - return new TestableRazorViewEngine( - pageFactory, - GetOptionsAccessor(expanders)); + return new TestableRazorViewEngine(pageFactory, GetOptionsAccessor(expanders)); } private static IOptions GetOptionsAccessor( - IEnumerable expanders = null) + IEnumerable expanders = null, + IEnumerable viewLocationFormats = null, + IEnumerable areaViewLocationFormats = null) { + var optionsSetup = new RazorViewEngineOptionsSetup(Mock.Of()); + var options = new RazorViewEngineOptions(); + optionsSetup.Configure(options); + if (expanders != null) { foreach (var expander in expanders) @@ -1730,6 +1701,26 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test } } + if (viewLocationFormats != null) + { + options.ViewLocationFormats.Clear(); + + foreach (var location in viewLocationFormats) + { + options.ViewLocationFormats.Add(location); + } + } + + if (areaViewLocationFormats != null) + { + options.AreaViewLocationFormats.Clear(); + + foreach (var location in areaViewLocationFormats) + { + options.AreaViewLocationFormats.Add(location); + } + } + var optionsAccessor = new Mock>(); optionsAccessor .SetupGet(v => v.Value) @@ -1784,9 +1775,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test private class TestableRazorViewEngine : RazorViewEngine { - private IEnumerable _viewLocationFormats; - private IEnumerable _areaViewLocationFormats; - public TestableRazorViewEngine( IRazorPageFactoryProvider pageFactory, IOptions optionsAccessor) @@ -1794,20 +1782,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test { } - public void SetLocationFormats( - IEnumerable viewLocationFormats, - IEnumerable areaViewLocationFormats) - { - _viewLocationFormats = viewLocationFormats; - _areaViewLocationFormats = areaViewLocationFormats; - } - - public override IEnumerable ViewLocationFormats => - _viewLocationFormats != null ? _viewLocationFormats : base.ViewLocationFormats; - - public override IEnumerable AreaViewLocationFormats => - _areaViewLocationFormats != null ? _areaViewLocationFormats : base.AreaViewLocationFormats; - public IMemoryCache ViewLookupCachePublic => ViewLookupCache; } } diff --git a/test/Microsoft.AspNetCore.Mvc.Test/RazorViewEngineOptionsSetupTest.cs b/test/Microsoft.AspNetCore.Mvc.Test/RazorViewEngineOptionsSetupTest.cs index f47ee5c334..5969526041 100644 --- a/test/Microsoft.AspNetCore.Mvc.Test/RazorViewEngineOptionsSetupTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Test/RazorViewEngineOptionsSetupTest.cs @@ -1,12 +1,10 @@ // 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.IO; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.CodeAnalysis; using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.PlatformAbstractions; using Moq; using Xunit;