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": {