diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/AttributeRouteModel.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/AttributeRouteModel.cs index 7331898262..32ca4d4142 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/AttributeRouteModel.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/AttributeRouteModel.cs @@ -99,6 +99,33 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels }; } + /// + /// Combines the prefix and route template for an attribute route. + /// + /// The prefix. + /// The route template. + /// The combined pattern. + public static string CombineTemplates(string prefix, string template) + { + var result = CombineCore(prefix, template); + return CleanTemplate(result); + } + + /// + /// Determines if a template pattern can be used to override a prefix. + /// + /// The template. + /// true if this is an overriding template, false otherwise. + /// + /// Route templates starting with "~/" or "/" can be used to override the prefix. + /// + public static bool IsOverridePattern(string template) + { + return template != null && + (template.StartsWith("~/", StringComparison.Ordinal) || + template.StartsWith("/", StringComparison.Ordinal)); + } + private static string ChooseName( AttributeRouteModel left, AttributeRouteModel right) @@ -113,12 +140,6 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels } } - internal static string CombineTemplates(string left, string right) - { - var result = CombineCore(left, right); - return CleanTemplate(result); - } - private static string CombineCore(string left, string right) { if (left == null && right == null) @@ -143,13 +164,6 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels return left + "/" + right; } - private static bool IsOverridePattern(string template) - { - return template != null && - (template.StartsWith("~/", StringComparison.Ordinal) || - template.StartsWith("/", StringComparison.Ordinal)); - } - private static bool IsEmptyLeftSegment(string template) { return template == null || diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionDescriptorBuilder.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionDescriptorBuilder.cs index dba88b0c20..13ff927b6b 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionDescriptorBuilder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionDescriptorBuilder.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; using System.Linq; using System.Reflection; using Microsoft.AspNetCore.Mvc.Abstractions; @@ -14,7 +13,6 @@ using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Core; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Routing; -using Microsoft.AspNetCore.Routing.Tree; namespace Microsoft.AspNetCore.Mvc.Internal { diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs index 618765631f..95ff1316b4 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs @@ -1306,22 +1306,6 @@ namespace Microsoft.AspNetCore.Mvc.Core return string.Format(CultureInfo.CurrentCulture, GetString("MiddlewareFilter_ServiceResolutionFail"), p0, p1, p2, p3); } - /// - /// Unable to create an instance of type '{0}'. The type specified in {1} must not be abstract and must have a parameterless constructor. - /// - internal static string MiddlewareFilterConfigurationProvider_CreateConfigureDelegate_CannotCreateType - { - get { return GetString("MiddlewareFilterConfigurationProvider_CreateConfigureDelegate_CannotCreateType"); } - } - - /// - /// Unable to create an instance of type '{0}'. The type specified in {1} must not be abstract and must have a parameterless constructor. - /// - internal static string FormatMiddlewareFilterConfigurationProvider_CreateConfigureDelegate_CannotCreateType(object p0, object p1) - { - return string.Format(CultureInfo.CurrentCulture, GetString("MiddlewareFilterConfigurationProvider_CreateConfigureDelegate_CannotCreateType"), p0, p1); - } - /// /// An {0} cannot be created without a valid instance of {1}. /// @@ -1355,7 +1339,7 @@ namespace Microsoft.AspNetCore.Mvc.Core } /// - /// VaryByQueryKeys requires the response cache middleware. + /// '{0}' requires the response cache middleware. /// internal static string VaryByQueryKeys_Requires_ResponseCachingMiddleware { @@ -1363,7 +1347,7 @@ namespace Microsoft.AspNetCore.Mvc.Core } /// - /// VaryByQueryKeys requires the response cache middleware. + /// '{0}' requires the response cache middleware. /// internal static string FormatVaryByQueryKeys_Requires_ResponseCachingMiddleware(object p0) { @@ -1386,6 +1370,22 @@ namespace Microsoft.AspNetCore.Mvc.Core return string.Format(CultureInfo.CurrentCulture, GetString("CandidateResolver_DifferentCasedReference"), p0); } + /// + /// Unable to create an instance of type '{0}'. The type specified in {1} must not be abstract and must have a parameterless constructor. + /// + internal static string MiddlewareFilterConfigurationProvider_CreateConfigureDelegate_CannotCreateType + { + get { return GetString("MiddlewareFilterConfigurationProvider_CreateConfigureDelegate_CannotCreateType"); } + } + + /// + /// Unable to create an instance of type '{0}'. The type specified in {1} must not be abstract and must have a parameterless constructor. + /// + internal static string FormatMiddlewareFilterConfigurationProvider_CreateConfigureDelegate_CannotCreateType(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("MiddlewareFilterConfigurationProvider_CreateConfigureDelegate_CannotCreateType"), p0, p1); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/IPageModelConvention.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/IPageModelConvention.cs new file mode 100644 index 0000000000..752cd81f49 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/IPageModelConvention.cs @@ -0,0 +1,17 @@ +// 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. + +namespace Microsoft.AspNetCore.Mvc.ApplicationModels +{ + /// + /// Allows customization of the of the . + /// + public interface IPageModelConvention + { + /// + /// Called to apply the convention to the . + /// + /// The . + void Apply(PageModel model); + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageModel.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageModel.cs new file mode 100644 index 0000000000..9c080aed1c --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageModel.cs @@ -0,0 +1,86 @@ +// 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.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Microsoft.AspNetCore.Mvc.ApplicationModels +{ + /// + /// Application model component for RazorPages. + /// + public class PageModel + { + /// + /// Initializes a new instance of . + /// + /// The application relative path of the page. + /// The path relative to the base path for page discovery. + public PageModel(string relativePath, string viewEnginePath) + { + if (relativePath == null) + { + throw new ArgumentNullException(nameof(relativePath)); + } + + if (viewEnginePath == null) + { + throw new ArgumentNullException(nameof(viewEnginePath)); + } + + RelativePath = relativePath; + ViewEnginePath = viewEnginePath; + + Filters = new List(); + Properties = new Dictionary(); + Selectors = new List(); + } + + /// + /// A copy constructor for . + /// + /// The to copy from. + public PageModel(PageModel other) + { + if (other == null) + { + throw new ArgumentNullException(nameof(other)); + } + + RelativePath = other.RelativePath; + ViewEnginePath = other.ViewEnginePath; + + Filters = new List(other.Filters); + Properties = new Dictionary(other.Properties); + + Selectors = new List(other.Selectors.Select(m => new SelectorModel(m))); + } + + /// + /// Gets or sets the application root relative path for the page. + /// + public string RelativePath { get; } + + /// + /// Gets or sets the path relative to the base path for page discovery. + /// + public string ViewEnginePath { get; } + + /// + /// Gets or sets the applicable instances. + /// + public IList Filters { get; } + + /// + /// Stores arbitrary metadata properties associated with the . + /// + public IDictionary Properties { get; } + + /// + /// Gets or sets the instances. + /// + public IList Selectors { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs new file mode 100644 index 0000000000..e1ff6a6e45 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs @@ -0,0 +1,127 @@ +// 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.Collections.Generic; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Razor.Evolution; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure +{ + public class PageActionDescriptorProvider : IActionDescriptorProvider + { + private static readonly string IndexFileName = "Index.cshtml"; + private readonly RazorProject _project; + private readonly MvcOptions _mvcOptions; + private readonly RazorPagesOptions _pagesOptions; + + public PageActionDescriptorProvider( + RazorProject project, + IOptions mvcOptionsAccessor, + IOptions pagesOptionsAccessor) + { + _project = project; + _mvcOptions = mvcOptionsAccessor.Value; + _pagesOptions = pagesOptionsAccessor.Value; + } + + public int Order { get; set; } + + public void OnProvidersExecuting(ActionDescriptorProviderContext context) + { + foreach (var item in _project.EnumerateItems("/")) + { + if (item.Filename.StartsWith("_")) + { + // Pages like _PageImports should not be routable. + continue; + } + + string template; + if (!PageDirectiveFeature.TryGetRouteTemplate(item, out template)) + { + // .cshtml pages without @page are not RazorPages. + continue; + } + + if (AttributeRouteModel.IsOverridePattern(template)) + { + throw new InvalidOperationException(string.Format( + Resources.PageActionDescriptorProvider_RouteTemplateCannotBeOverrideable, + item.Path)); + } + + AddActionDescriptors(context.Results, item, template); + } + } + + public void OnProvidersExecuted(ActionDescriptorProviderContext context) + { + } + + private void AddActionDescriptors(IList actions, RazorProjectItem item, string template) + { + var model = new PageModel(item.CombinedPath, item.PathWithoutExtension); + var routePrefix = item.BasePath == "/" ? item.PathWithoutExtension : item.BasePath + item.PathWithoutExtension; + model.Selectors.Add(CreateSelectorModel(routePrefix, template)); + + if (string.Equals(IndexFileName, item.Filename, StringComparison.OrdinalIgnoreCase)) + { + model.Selectors.Add(CreateSelectorModel(item.BasePath, template)); + } + + for (var i = 0; i < _pagesOptions.Conventions.Count; i++) + { + _pagesOptions.Conventions[i].Apply(model); + } + + var filters = new List(_mvcOptions.Filters.Count + model.Filters.Count); + for (var i = 0; i < _mvcOptions.Filters.Count; i++) + { + filters.Add(new FilterDescriptor(_mvcOptions.Filters[i], FilterScope.Global)); + } + + for (var i = 0; i < model.Filters.Count; i++) + { + filters.Add(new FilterDescriptor(model.Filters[i], FilterScope.Action)); + } + + foreach (var selector in model.Selectors) + { + actions.Add(new PageActionDescriptor() + { + AttributeRouteInfo = new AttributeRouteInfo() + { + Name = selector.AttributeRouteModel.Name, + Order = selector.AttributeRouteModel.Order ?? 0, + Template = selector.AttributeRouteModel.Template, + }, + DisplayName = $"Page: {item.Path}", + FilterDescriptors = filters, + Properties = new Dictionary(model.Properties), + RelativePath = item.CombinedPath, + RouteValues = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "page", item.PathWithoutExtension }, + }, + ViewEnginePath = item.Path, + }); + } + } + + private static SelectorModel CreateSelectorModel(string prefix, string template) + { + return new SelectorModel + { + AttributeRouteModel = new AttributeRouteModel + { + Template = AttributeRouteModel.CombineTemplates(prefix, template), + } + }; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageDirectiveFeature.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageDirectiveFeature.cs new file mode 100644 index 0000000000..64dc2c0e9b --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageDirectiveFeature.cs @@ -0,0 +1,33 @@ +// 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.IO; +using Microsoft.AspNetCore.Razor.Evolution; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure +{ + public static class PageDirectiveFeature + { + public static bool TryGetRouteTemplate(RazorProjectItem projectItem, out string template) + { + const string PageDirective = "@page"; + + string content; + using (var streamReader = new StreamReader(projectItem.Read())) + { + content = streamReader.ReadToEnd(); + } + + if (content.StartsWith(PageDirective, StringComparison.Ordinal)) + { + var newLineIndex = content.IndexOf(Environment.NewLine, PageDirective.Length); + template = content.Substring(PageDirective.Length, newLineIndex - PageDirective.Length).Trim(); + return true; + } + + template = null; + return false; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultRazorProject.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultRazorProject.cs new file mode 100644 index 0000000000..06c6347bf5 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultRazorProject.cs @@ -0,0 +1,59 @@ +// 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.Collections.Generic; +using System.IO; +using Microsoft.AspNetCore.Razor.Evolution; +using Microsoft.Extensions.FileProviders; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public class DefaultRazorProject : RazorProject + { + private const string RazorFileExtension = ".cshtml"; + private readonly IFileProvider _provider; + + public DefaultRazorProject(IFileProvider provider) + { + _provider = provider; + } + + public override IEnumerable EnumerateItems(string path) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + if (path.Length == 0 || path[0] != '/') + { + throw new ArgumentException(Resources.RazorProject_PathMustStartWithForwardSlash); + } + + return EnumerateFiles(_provider.GetDirectoryContents(path), path, ""); + } + + private IEnumerable EnumerateFiles(IDirectoryContents directory, string basePath, string prefix) + { + if (directory.Exists) + { + foreach (var file in directory) + { + if (file.IsDirectory) + { + var children = EnumerateFiles(_provider.GetDirectoryContents(file.PhysicalPath), basePath, prefix + "/" + file.Name); + foreach (var child in children) + { + yield return child; + } + } + else if (string.Equals(RazorFileExtension, Path.GetExtension(file.Name), StringComparison.OrdinalIgnoreCase)) + { + yield return new DefaultRazorProjectItem(file, basePath, prefix + "/" + file.Name); + } + } + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultRazorProjectItem.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultRazorProjectItem.cs new file mode 100644 index 0000000000..ff3451ea99 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultRazorProjectItem.cs @@ -0,0 +1,32 @@ +// 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.Razor.Evolution; +using Microsoft.Extensions.FileProviders; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public class DefaultRazorProjectItem : RazorProjectItem + { + private readonly IFileInfo _fileInfo; + + public DefaultRazorProjectItem(IFileInfo fileInfo, string basePath, string path) + { + _fileInfo = fileInfo; + BasePath = basePath; + Path = path; + } + + public override string BasePath { get; } + + public override string Path { get; } + + public override string PhysicalPath => _fileInfo.PhysicalPath; + + public override Stream Read() + { + return _fileInfo.CreateReadStream(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/PageActionDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageActionDescriptor.cs new file mode 100644 index 0000000000..bcaa23235b --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageActionDescriptor.cs @@ -0,0 +1,16 @@ +// 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.Diagnostics; +using Microsoft.AspNetCore.Mvc.Abstractions; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure +{ + [DebuggerDisplay("{" + nameof(ViewEnginePath) + "}")] + public class PageActionDescriptor : ActionDescriptor + { + public string RelativePath { get; set; } + + public string ViewEnginePath { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..07d93675dc --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs @@ -0,0 +1,62 @@ +// +namespace Microsoft.AspNetCore.Mvc.RazorPages +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNetCore.Mvc.RazorPages.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// The route for the page at '{0}' cannot start with / or ~/. Pages do not support overriding the file path of the page. + /// + internal static string PageActionDescriptorProvider_RouteTemplateCannotBeOverrideable + { + get { return GetString("PageActionDescriptorProvider_RouteTemplateCannotBeOverrideable"); } + } + + /// + /// The route for the page at '{0}' cannot start with / or ~/. Pages do not support overriding the file path of the page. + /// + internal static string FormatPageActionDescriptorProvider_RouteTemplateCannotBeOverrideable(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("PageActionDescriptorProvider_RouteTemplateCannotBeOverrideable"), p0); + } + + /// + /// Path must begin with a forward slash '/'. + /// + internal static string RazorProject_PathMustStartWithForwardSlash + { + get { return GetString("RazorProject_PathMustStartWithForwardSlash"); } + } + + /// + /// Path must begin with a forward slash '/'. + /// + internal static string FormatRazorProject_PathMustStartWithForwardSlash() + { + return GetString("RazorProject_PathMustStartWithForwardSlash"); + } + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/RazorPagesOptions.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/RazorPagesOptions.cs new file mode 100644 index 0000000000..4d931a796c --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/RazorPagesOptions.cs @@ -0,0 +1,20 @@ +// 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.ApplicationModels; + +namespace Microsoft.AspNetCore.Mvc.RazorPages +{ + /// + /// Provides configuration for RazorPages. + /// + public class RazorPagesOptions + { + /// + /// Gets a list of instances that will be applied to + /// the when discovering Razor Pages. + /// + public IList Conventions { get; } = new List(); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx b/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx new file mode 100644 index 0000000000..2679a194f5 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The route for the page at '{0}' cannot start with / or ~/. Pages do not support overriding the file path of the page. + + + Path must begin with a forward slash '/'. + + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/project.json b/src/Microsoft.AspNetCore.Mvc.RazorPages/project.json index d4ed1c6e4c..670c32082d 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/project.json +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/project.json @@ -22,6 +22,7 @@ "xmlDoc": true }, "dependencies": { + "Microsoft.AspNetCore.Mvc.ViewFeatures": { "target": "project" }, "Microsoft.AspNetCore.Razor.Evolution": "1.0.0-*", "NETStandard.Library": "1.6.1-*" }, diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/Properties/Resources.Designer.cs index d2d8502c6f..5ef947f86e 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/Properties/Resources.Designer.cs @@ -139,7 +139,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers } /// - /// Cannot determine an '{4}' attribute for {0}. A {0} with a specified '{1}' must not have an '{2}' or '{3}' attribute. + /// Cannot determine an '{4}' attribute for {0}. A {0} with a specified '{1}' must not have an '{2}', '{3}', or '{5}' attribute. /// internal static string FormTagHelper_CannotDetermineActionWithRouteAndActionOrControllerSpecified { @@ -171,7 +171,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers } /// - /// Cannot override the '{6}' attribute for <{0}>. <{0}> elements with a specified '{6}' must not have attributes starting with '{5}' or an '{1}', '{2}', '{3}', or '{4}' attribute. + /// Cannot override the '{7}' attribute for <{0}>. <{0}> elements with a specified '{7}' must not have attributes starting with '{6}' or an '{1}', '{2}', '{3}', '{4}', or '{5}' attribute. /// internal static string FormActionTagHelper_CannotOverrideFormAction { @@ -179,7 +179,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers } /// - /// Cannot override the '{6}' attribute for <{0}>. <{0}> elements with a specified '{7}' must not have attributes starting with '{6}' or an '{1}', '{2}', '{3}', '{4}', or '{5}' attribute. + /// Cannot override the '{7}' attribute for <{0}>. <{0}> elements with a specified '{7}' must not have attributes starting with '{6}' or an '{1}', '{2}', '{3}', '{4}', or '{5}' attribute. /// internal static string FormatFormActionTagHelper_CannotOverrideFormAction(object p0, object p1, object p2, object p3, object p4, object p5, object p6, object p7) { @@ -187,7 +187,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers } /// - /// Cannot determine a '{4}' attribute for <{0}>. <{0}> elements with a specified '{1}' must not have an '{2}' or '{3}' attribute. + /// Cannot determine a '{4}' attribute for <{0}>. <{0}> elements with a specified '{1}' must not have an '{2}', '{3}', or '{5}' attribute. /// internal static string FormActionTagHelper_CannotDetermineFormActionRouteActionOrControllerSpecified { diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionDescriptorProviderTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionDescriptorProviderTests.cs index 98d1c9ef0e..faa86f90b2 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionDescriptorProviderTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionDescriptorProviderTests.cs @@ -14,7 +14,6 @@ using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Routing; -using Microsoft.AspNetCore.Routing.Tree; using Moq; using Xunit; diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/PageActionDescriptorProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/PageActionDescriptorProviderTest.cs new file mode 100644 index 0000000000..b1040ac812 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/PageActionDescriptorProviderTest.cs @@ -0,0 +1,304 @@ +// 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 Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Razor; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.RazorPages.Internal; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; +using Microsoft.AspNetCore.Razor.Evolution; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure +{ + public class PageActionDescriptorProviderTest + { + [Fact] + public void GetDescriptors_DoesNotAddDescriptorsForFilesWithoutDirectives() + { + // Arrange + var razorProject = new Mock(); + razorProject.Setup(p => p.EnumerateItems("/")) + .Returns(new[] + { + GetProjectItem("/", "/Index.cshtml", "

Hello world

"), + }); + var provider = new PageActionDescriptorProvider( + razorProject.Object, + GetAccessor(), + GetAccessor()); + var context = new ActionDescriptorProviderContext(); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + Assert.Empty(context.Results); + } + + [Fact] + public void GetDescriptors_AddsDescriptorsForFileWithPageDirective() + { + // Arrange + var razorProject = new Mock(); + razorProject.Setup(p => p.EnumerateItems("/")) + .Returns(new[] + { + GetProjectItem("/", "/Test.cshtml", $"@page{Environment.NewLine}

Hello world

"), + }); + var provider = new PageActionDescriptorProvider( + razorProject.Object, + GetAccessor(), + GetAccessor()); + var context = new ActionDescriptorProviderContext(); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + var result = Assert.Single(context.Results); + var descriptor = Assert.IsType(result); + Assert.Equal("/Test.cshtml", descriptor.RelativePath); + Assert.Equal("/Test", descriptor.RouteValues["page"]); + Assert.Equal("Test", descriptor.AttributeRouteInfo.Template); + } + + [Fact] + public void GetDescriptors_AddsDescriptorsForFileWithPageDirectiveAndRouteTemplate() + { + // Arrange + var razorProject = new Mock(); + razorProject.Setup(p => p.EnumerateItems("/")) + .Returns(new[] + { + GetProjectItem("/", "/Test.cshtml", $"@page Home {Environment.NewLine}

Hello world

"), + }); + var provider = new PageActionDescriptorProvider( + razorProject.Object, + GetAccessor(), + GetAccessor()); + var context = new ActionDescriptorProviderContext(); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + var result = Assert.Single(context.Results); + var descriptor = Assert.IsType(result); + Assert.Equal("/Test.cshtml", descriptor.RelativePath); + Assert.Equal("/Test", descriptor.RouteValues["page"]); + Assert.Equal("Test/Home", descriptor.AttributeRouteInfo.Template); + } + + [Theory] + [InlineData("/Path1")] + [InlineData("~/Path1")] + public void GetDescriptors_ThrowsIfRouteTemplatesAreOverriden(string template) + { + // Arrange + var razorProject = new Mock(); + razorProject.Setup(p => p.EnumerateItems("/")) + .Returns(new[] + { + GetProjectItem("/", "/Test.cshtml", $"@page {template} {Environment.NewLine}

Hello world

"), + }); + var provider = new PageActionDescriptorProvider( + razorProject.Object, + GetAccessor(), + GetAccessor()); + var context = new ActionDescriptorProviderContext(); + + // Act and Assert + var ex = Assert.Throws(() => provider.OnProvidersExecuting(context)); + Assert.Equal( + "The route for the page at '/Test.cshtml' cannot start with / or ~/. " + + "Pages do not support overriding the file path of the page.", + ex.Message); + } + + [Fact] + public void GetDescriptors_WithEmptyPageDirective_MapsIndexToEmptySegment() + { + // Arrange + var razorProject = new Mock(); + razorProject.Setup(p => p.EnumerateItems("/")) + .Returns(new[] + { + GetProjectItem("/About", "/Index.cshtml", $"@page {Environment.NewLine}"), + }); + var provider = new PageActionDescriptorProvider( + razorProject.Object, + GetAccessor(), + GetAccessor()); + var context = new ActionDescriptorProviderContext(); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + Assert.Collection(context.Results, + result => + { + var descriptor = Assert.IsType(result); + Assert.Equal("/About/Index.cshtml", descriptor.RelativePath); + Assert.Equal("/Index", descriptor.RouteValues["page"]); + Assert.Equal("About/Index", descriptor.AttributeRouteInfo.Template); + }, + result => + { + var descriptor = Assert.IsType(result); + Assert.Equal("/About/Index.cshtml", descriptor.RelativePath); + Assert.Equal("/Index", descriptor.RouteValues["page"]); + Assert.Equal("About", descriptor.AttributeRouteInfo.Template); + }); + } + + [Fact] + public void GetDescriptors_WithNonEmptyPageDirective_MapsIndexToEmptySegment() + { + // Arrange + var razorProject = new Mock(); + razorProject.Setup(p => p.EnumerateItems("/")) + .Returns(new[] + { + GetProjectItem("/Catalog/Details", "/Index.cshtml", $"@page {{id:int?}} {Environment.NewLine}"), + }); + var provider = new PageActionDescriptorProvider( + razorProject.Object, + GetAccessor(), + GetAccessor()); + var context = new ActionDescriptorProviderContext(); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + Assert.Collection(context.Results, + result => + { + var descriptor = Assert.IsType(result); + Assert.Equal("/Catalog/Details/Index.cshtml", descriptor.RelativePath); + Assert.Equal("/Index", descriptor.RouteValues["page"]); + Assert.Equal("Catalog/Details/Index/{id:int?}", descriptor.AttributeRouteInfo.Template); + }, + result => + { + var descriptor = Assert.IsType(result); + Assert.Equal("/Catalog/Details/Index.cshtml", descriptor.RelativePath); + Assert.Equal("/Index", descriptor.RouteValues["page"]); + Assert.Equal("Catalog/Details/{id:int?}", descriptor.AttributeRouteInfo.Template); + }); + } + + [Fact] + public void GetDescriptors_AddsGlobalFilters() + { + // Arrange + var filter1 = Mock.Of(); + var filter2 = Mock.Of(); + var options = new MvcOptions(); + options.Filters.Add(filter1); + options.Filters.Add(filter2); + var razorProject = new Mock(); + razorProject.Setup(p => p.EnumerateItems("/")) + .Returns(new[] + { + GetProjectItem("/", "/Home.cshtml", $"@page {Environment.NewLine}"), + }); + var provider = new PageActionDescriptorProvider( + razorProject.Object, + GetAccessor(options), + GetAccessor()); + var context = new ActionDescriptorProviderContext(); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + var result = Assert.Single(context.Results); + var descriptor = Assert.IsType(result); + Assert.Collection(descriptor.FilterDescriptors, + filterDescriptor => + { + Assert.Equal(FilterScope.Global, filterDescriptor.Scope); + Assert.Same(filter1, filterDescriptor.Filter); + }, + filterDescriptor => + { + Assert.Equal(FilterScope.Global, filterDescriptor.Scope); + Assert.Same(filter2, filterDescriptor.Filter); + }); + } + + [Fact] + public void GetDescriptors_AddsFiltersAddedByConvention() + { + // Arrange + var globalFilter = Mock.Of(); + var localFilter = Mock.Of(); + var options = new MvcOptions(); + options.Filters.Add(globalFilter); + var convention = new Mock(); + convention.Setup(c => c.Apply(It.IsAny())) + .Callback((PageModel model) => + { + model.Filters.Add(localFilter); + }); + var razorOptions = new RazorPagesOptions(); + razorOptions.Conventions.Add(convention.Object); + + var razorProject = new Mock(); + razorProject.Setup(p => p.EnumerateItems("/")) + .Returns(new[] + { + GetProjectItem("/", "/Home.cshtml", $"@page {Environment.NewLine}"), + }); + var provider = new PageActionDescriptorProvider( + razorProject.Object, + GetAccessor(options), + GetAccessor(razorOptions)); + var context = new ActionDescriptorProviderContext(); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + var result = Assert.Single(context.Results); + var descriptor = Assert.IsType(result); + Assert.Collection(descriptor.FilterDescriptors, + filterDescriptor => + { + Assert.Equal(FilterScope.Global, filterDescriptor.Scope); + Assert.Same(globalFilter, filterDescriptor.Filter); + }, + filterDescriptor => + { + Assert.Equal(FilterScope.Action, filterDescriptor.Scope); + Assert.Same(localFilter, filterDescriptor.Filter); + }); + } + + private static IOptions GetAccessor(TOptions options = null) + where TOptions : class, new() + { + var accessor = new Mock>(); + accessor.SetupGet(a => a.Value).Returns(options ?? new TOptions()); + return accessor.Object; + } + + private static RazorProjectItem GetProjectItem(string basePath, string path, string content) + { + var testFileInfo = new TestFileInfo + { + Content = content, + }; + + return new DefaultRazorProjectItem(testFileInfo, basePath, path); + } + + + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Microsoft.AspNetCore.Mvc.RazorPages.Test.xproj b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Microsoft.AspNetCore.Mvc.RazorPages.Test.xproj index 312e8c3745..9ce0ba78bc 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Microsoft.AspNetCore.Mvc.RazorPages.Test.xproj +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Microsoft.AspNetCore.Mvc.RazorPages.Test.xproj @@ -4,7 +4,6 @@ 14.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - 0ab46520-f441-4e01-b444-08f4d23f8b1b @@ -13,9 +12,11 @@ .\bin\ v4.6 - 2.0 + + + - + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/project.json b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/project.json index 94cac77a2c..00a18a652c 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/project.json +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/project.json @@ -4,11 +4,15 @@ }, "dependencies": { "dotnet-test-xunit": "2.2.0-*", - "Microsoft.AspNetCore.Testing": "1.2.0-*", + "Microsoft.AspNetCore.Mvc.RazorPages": "1.0.0-*", + "Microsoft.AspNetCore.Mvc.TestCommon": { + "target": "project" + }, "Microsoft.DotNet.InternalAbstractions": "1.0.0", "Moq": "4.6.36-*", "xunit": "2.2.0-*" }, + "testRunner": "xunit", "frameworks": { "netcoreapp1.1": { "dependencies": {