From 9f3dfd98194d94362586bd5a8c1dccec9df38a48 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 31 Jan 2017 17:37:07 -0800 Subject: [PATCH] Make RazorPages work E2E --- .../DefaultAssemblyPartDiscoveryProvider.cs | 1 + .../BaseDocumentClassifierPass.cs | 177 ++++++ .../InjectDirective.cs | 9 +- .../ModelDirective.cs | 21 +- .../MvcViewDocumentClassifierPass.cs | 254 +-------- .../PageDirective.cs | 48 ++ .../RazorPageDocumentClassifier.cs | 26 + .../MvcRazorMvcCoreBuilderExtensions.cs | 2 + .../Internal/DefaultRazorProject.cs | 4 +- .../Internal/DefaultRazorProjectItem.cs | 12 +- .../RazorPage.cs | 501 +---------------- .../RazorPageBase.cs | 514 ++++++++++++++++++ .../MvcRazorPagesMvcCoreBuilderExtensions.cs | 71 +++ .../PageActionDescriptorProvider.cs | 12 +- .../Infrastructure/PageResultExecutor.cs | 4 +- .../DefaultPageHandlerMethodSelector.cs | 13 + .../Internal/DefaultPageLoader.cs | 40 ++ .../Internal/PageActionInvokerProvider.cs | 2 + .../Internal/PassThruRazorPageActivator.cs | 29 + .../Page.cs | 86 ++- .../project.json | 1 - .../MvcServiceCollectionExtensions.cs | 1 + src/Microsoft.AspNetCore.Mvc/project.json | 2 +- .../MvcSandboxTest.cs | 9 + .../RazorPageTest.cs | 2 +- .../Infrastructure/DefaultPageFactoryTest.cs | 2 +- .../PageActionDescriptorProviderTest.cs | 19 +- .../MvcServiceCollectionExtensionsTest.cs | 4 + .../Services/UpdateableFileProvider.cs | 5 +- 29 files changed, 1090 insertions(+), 781 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Mvc.Razor.Host/BaseDocumentClassifierPass.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Razor.Host/PageDirective.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Razor.Host/RazorPageDocumentClassifier.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Razor/RazorPageBase.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageHandlerMethodSelector.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageLoader.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PassThruRazorPageActivator.cs diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultAssemblyPartDiscoveryProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultAssemblyPartDiscoveryProvider.cs index e6df5bc816..0e37be8240 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultAssemblyPartDiscoveryProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultAssemblyPartDiscoveryProvider.cs @@ -28,6 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal "Microsoft.AspNetCore.Mvc.Localization", "Microsoft.AspNetCore.Mvc.Razor", "Microsoft.AspNetCore.Mvc.Razor.Host", + "Microsoft.AspNetCore.Mvc.RazorPages", "Microsoft.AspNetCore.Mvc.TagHelpers", "Microsoft.AspNetCore.Mvc.ViewFeatures" }; diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Host/BaseDocumentClassifierPass.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Host/BaseDocumentClassifierPass.cs new file mode 100644 index 0000000000..7bfb8bb393 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Host/BaseDocumentClassifierPass.cs @@ -0,0 +1,177 @@ +// 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 System.Globalization; +using System.IO; +using Microsoft.AspNetCore.Razor.Evolution; +using Microsoft.AspNetCore.Razor.Evolution.Intermediate; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Mvc.Razor.Host +{ + public abstract class BaseDocumentClassifierPass : IRazorIRPass + { + public RazorEngine Engine { get; set; } + + // We want to run before the default, but after others since this is the MVC default. + public virtual int Order => RazorIRPass.DefaultDocumentClassifierOrder - 1; + + protected abstract string BaseType { get; } + + public DocumentIRNode Execute(RazorCodeDocument codeDocument, DocumentIRNode irDocument) + { + if (irDocument.DocumentKind != null) + { + return irDocument; + } + + var documentKind = ClassifyDocument(codeDocument, irDocument); + if (documentKind == null) + { + return irDocument; + } + + irDocument.DocumentKind = documentKind; + + return ExecuteCore(codeDocument, irDocument); + } + + protected virtual DocumentIRNode ExecuteCore(RazorCodeDocument codeDocument, DocumentIRNode irDocument) + { + // Rewrite a use default namespace and class declaration. + var children = new List(irDocument.Children); + irDocument.Children.Clear(); + + var @namespace = new NamespaceDeclarationIRNode + { + Content = "AspNetCore", + }; + + var @class = new ClassDeclarationIRNode + { + AccessModifier = "public", + Name = GetClassName(codeDocument.Source.Filename) ?? "GeneratedClass", + BaseType = BaseType, + }; + + var method = new RazorMethodDeclarationIRNode() + { + AccessModifier = "public", + Modifiers = new List() { "async", "override" }, + Name = "ExecuteAsync", + ReturnType = "Task", + }; + + var documentBuilder = RazorIRBuilder.Create(irDocument); + + var namespaceBuilder = RazorIRBuilder.Create(documentBuilder.Current); + namespaceBuilder.Push(@namespace); + + var classBuilder = RazorIRBuilder.Create(namespaceBuilder.Current); + classBuilder.Push(@class); + + var methodBuilder = RazorIRBuilder.Create(classBuilder.Current); + methodBuilder.Push(method); + + var visitor = new Visitor(documentBuilder, namespaceBuilder, classBuilder, methodBuilder); + + for (var i = 0; i < children.Count; i++) + { + visitor.Visit(children[i]); + } + + return irDocument; + } + + protected abstract string ClassifyDocument(RazorCodeDocument codeDocument, DocumentIRNode irDocument); + + private static string GetClassName(string filename) + { + if (filename == null) + { + return null; + } + + return SanitizeClassName("Generated_" + Path.GetFileNameWithoutExtension(filename)); + } + + // CSharp Spec §2.4.2 + private static bool IsIdentifierStart(char character) + { + return char.IsLetter(character) || + character == '_' || + CharUnicodeInfo.GetUnicodeCategory(character) == UnicodeCategory.LetterNumber; + } + + public static bool IsIdentifierPart(char character) + { + return char.IsDigit(character) || + IsIdentifierStart(character) || + IsIdentifierPartByUnicodeCategory(character); + } + + private static bool IsIdentifierPartByUnicodeCategory(char character) + { + var category = CharUnicodeInfo.GetUnicodeCategory(character); + + return category == UnicodeCategory.NonSpacingMark || // Mn + category == UnicodeCategory.SpacingCombiningMark || // Mc + category == UnicodeCategory.ConnectorPunctuation || // Pc + category == UnicodeCategory.Format; // Cf + } + + public static string SanitizeClassName(string inputName) + { + if (!IsIdentifierStart(inputName[0]) && IsIdentifierPart(inputName[0])) + { + inputName = "_" + inputName; + } + + var builder = new InplaceStringBuilder(inputName.Length); + for (var i = 0; i < inputName.Length; i++) + { + var ch = inputName[i]; + builder.Append(IsIdentifierPart(ch) ? ch : '_'); + } + + return builder.ToString(); + } + + private class Visitor : RazorIRNodeVisitor + { + private readonly RazorIRBuilder _document; + private readonly RazorIRBuilder _namespace; + private readonly RazorIRBuilder _class; + private readonly RazorIRBuilder _method; + + public Visitor(RazorIRBuilder document, RazorIRBuilder @namespace, RazorIRBuilder @class, RazorIRBuilder method) + { + _document = document; + _namespace = @namespace; + _class = @class; + _method = method; + } + + public override void VisitChecksum(ChecksumIRNode node) + { + _document.Insert(0, node); + } + + public override void VisitUsingStatement(UsingStatementIRNode node) + { + _namespace.AddAfter(node); + } + + public override void VisitDeclareTagHelperFields(DeclareTagHelperFieldsIRNode node) + { + _class.Insert(0, node); + } + + public override void VisitDefault(RazorIRNode node) + { + _method.Add(node); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Host/InjectDirective.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Host/InjectDirective.cs index e37a029594..986e5eb191 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Host/InjectDirective.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Host/InjectDirective.cs @@ -30,6 +30,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Host { var visitor = new Visitor(); visitor.Visit(irDocument); + var modelType = ModelDirective.GetModelType(irDocument); var properties = new HashSet(StringComparer.Ordinal); @@ -50,8 +51,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Host continue; } - var modelType = ModelDirective.GetModelType(irDocument); - typeName = typeName.Replace("", "<" + modelType + ">"); var member = new CSharpStatementIRNode() @@ -74,8 +73,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Host public IList Directives { get; } = new List(); - public IList ModelType { get; } = new List(); - public override void VisitClass(ClassDeclarationIRNode node) { if (Class == null) @@ -92,11 +89,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Host { Directives.Add(node); } - else if (node.Descriptor == ModelDirective.Directive) - { - ModelType.Add(node); } } } - } } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Host/ModelDirective.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Host/ModelDirective.cs index 6d21a6b306..56d8e9b61b 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Host/ModelDirective.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Host/ModelDirective.cs @@ -28,13 +28,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Host } var visitor = new Visitor(); - visitor.Visit(document); - - return GetModelType(visitor); + return GetModelType(document, visitor); } - private static string GetModelType(Visitor visitor) + private static string GetModelType(DocumentIRNode document, Visitor visitor) { + visitor.Visit(document); + for (var i = visitor.ModelDirectives.Count - 1; i >= 0; i--) { var directive = visitor.ModelDirectives[i]; @@ -46,7 +46,14 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Host } } - return "dynamic"; + if (document.DocumentKind == RazorPageDocumentClassifier.DocumentKind) + { + return visitor.Class.Name; + } + else + { + return "dynamic"; + } } internal class Pass : IRazorIRPass @@ -59,9 +66,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Host public DocumentIRNode Execute(RazorCodeDocument codeDocument, DocumentIRNode irDocument) { var visitor = new Visitor(); - visitor.Visit(irDocument); - - var modelType = GetModelType(visitor); + var modelType = GetModelType(irDocument, visitor); var baseType = visitor.Class.BaseType; for (var i = visitor.InheritsDirectives.Count - 1; i >= 0; i--) diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Host/MvcViewDocumentClassifierPass.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Host/MvcViewDocumentClassifierPass.cs index f1fdfde546..792de7eea3 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Host/MvcViewDocumentClassifierPass.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Host/MvcViewDocumentClassifierPass.cs @@ -1,262 +1,18 @@ // 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.Globalization; -using System.IO; -using System.Linq; using Microsoft.AspNetCore.Razor.Evolution; using Microsoft.AspNetCore.Razor.Evolution.Intermediate; namespace Microsoft.AspNetCore.Mvc.Razor.Host { - public class MvcViewDocumentClassifierPass : IRazorIRPass + public class MvcViewDocumentClassifierPass : BaseDocumentClassifierPass { - public static readonly string Kind = "mvc.1.0.view"; + public static readonly string DocumentKind = "mvc.1.0.view"; - public RazorEngine Engine { get; set; } + protected override string BaseType => "Microsoft.AspNetCore.Mvc.Razor.RazorPage"; - // We want to run before the default, but after others since this is the MVC default. - public virtual int Order => RazorIRPass.DefaultDocumentClassifierOrder - 1; - - public static string DocumentKind = "default"; - - public DocumentIRNode Execute(RazorCodeDocument codeDocument, DocumentIRNode irDocument) - { - if (irDocument.DocumentKind != null) - { - return irDocument; - } - - irDocument.DocumentKind = DocumentKind; - - // Rewrite a use default namespace and class declaration. - var children = new List(irDocument.Children); - irDocument.Children.Clear(); - - var @namespace = new NamespaceDeclarationIRNode() - { - Content = "AspNetCore", - }; - - var @class = new ClassDeclarationIRNode() - { - AccessModifier = "public", - Name = GetClassName(codeDocument.Source.Filename) ?? "GeneratedClass", - BaseType = "Microsoft.AspNetCore.Mvc.Razor.RazorPage" - }; - - var method = new RazorMethodDeclarationIRNode() - { - AccessModifier = "public", - Modifiers = new List() { "async", "override" }, - Name = "ExecuteAsync", - ReturnType = "Task", - }; - - var documentBuilder = RazorIRBuilder.Create(irDocument); - - var namespaceBuilder = RazorIRBuilder.Create(documentBuilder.Current); - namespaceBuilder.Push(@namespace); - - var classBuilder = RazorIRBuilder.Create(namespaceBuilder.Current); - classBuilder.Push(@class); - - var methodBuilder = RazorIRBuilder.Create(classBuilder.Current); - methodBuilder.Push(method); - - var visitor = new Visitor(documentBuilder, namespaceBuilder, classBuilder, methodBuilder); - - for (var i = 0; i < children.Count; i++) - { - visitor.Visit(children[i]); - } - - return irDocument; - } - - private static string GetClassName(string filename) - { - if (filename == null) - { - return null; - } - - return ParserHelpers.SanitizeClassName("Generated_" + Path.GetFileNameWithoutExtension(filename)); - } - - private static class ParserHelpers - { - public static bool IsNewLine(char value) - { - return value == '\r' // Carriage return - || value == '\n' // Linefeed - || value == '\u0085' // Next Line - || value == '\u2028' // Line separator - || value == '\u2029'; // Paragraph separator - } - - public static bool IsNewLine(string value) - { - return (value.Length == 1 && (IsNewLine(value[0]))) || - (string.Equals(value, Environment.NewLine, StringComparison.Ordinal)); - } - - // Returns true if the character is Whitespace and NOT a newline - public static bool IsWhitespace(char value) - { - return value == ' ' || - value == '\f' || - value == '\t' || - value == '\u000B' || // Vertical Tab - CharUnicodeInfo.GetUnicodeCategory(value) == UnicodeCategory.SpaceSeparator; - } - - public static bool IsWhitespaceOrNewLine(char value) - { - return IsWhitespace(value) || IsNewLine(value); - } - - public static bool IsIdentifier(string value) - { - return IsIdentifier(value, requireIdentifierStart: true); - } - - public static bool IsIdentifier(string value, bool requireIdentifierStart) - { - IEnumerable identifierPart = value; - if (requireIdentifierStart) - { - identifierPart = identifierPart.Skip(1); - } - return (!requireIdentifierStart || IsIdentifierStart(value[0])) && identifierPart.All(IsIdentifierPart); - } - - public static bool IsHexDigit(char value) - { - return (value >= '0' && value <= '9') || (value >= 'A' && value <= 'F') || (value >= 'a' && value <= 'f'); - } - - public static bool IsIdentifierStart(char value) - { - return value == '_' || IsLetter(value); - } - - public static bool IsIdentifierPart(char value) - { - return IsLetter(value) - || IsDecimalDigit(value) - || IsConnecting(value) - || IsCombining(value) - || IsFormatting(value); - } - - public static bool IsTerminatingCharToken(char value) - { - return IsNewLine(value) || value == '\''; - } - - public static bool IsTerminatingQuotedStringToken(char value) - { - return IsNewLine(value) || value == '"'; - } - - public static bool IsDecimalDigit(char value) - { - return CharUnicodeInfo.GetUnicodeCategory(value) == UnicodeCategory.DecimalDigitNumber; - } - - public static bool IsLetterOrDecimalDigit(char value) - { - return IsLetter(value) || IsDecimalDigit(value); - } - - public static bool IsLetter(char value) - { - var cat = CharUnicodeInfo.GetUnicodeCategory(value); - - return cat == UnicodeCategory.UppercaseLetter - || cat == UnicodeCategory.LowercaseLetter - || cat == UnicodeCategory.TitlecaseLetter - || cat == UnicodeCategory.ModifierLetter - || cat == UnicodeCategory.OtherLetter - || cat == UnicodeCategory.LetterNumber; - } - - public static bool IsFormatting(char value) - { - return CharUnicodeInfo.GetUnicodeCategory(value) == UnicodeCategory.Format; - } - - public static bool IsCombining(char value) - { - UnicodeCategory cat = CharUnicodeInfo.GetUnicodeCategory(value); - - return cat == UnicodeCategory.SpacingCombiningMark || cat == UnicodeCategory.NonSpacingMark; - - } - - public static bool IsConnecting(char value) - { - return CharUnicodeInfo.GetUnicodeCategory(value) == UnicodeCategory.ConnectorPunctuation; - } - - public static string SanitizeClassName(string inputName) - { - if (!IsIdentifierStart(inputName[0]) && IsIdentifierPart(inputName[0])) - { - inputName = "_" + inputName; - } - - return new String((from value in inputName - select IsIdentifierPart(value) ? value : '_') - .ToArray()); - } - - public static bool IsEmailPart(char character) - { - // Source: http://tools.ietf.org/html/rfc5322#section-3.4.1 - // We restrict the allowed characters to alpha-numerics and '_' in order to ensure we cover most of the cases where an - // email address is intended without restricting the usage of code within JavaScript, CSS, and other contexts. - return Char.IsLetter(character) || Char.IsDigit(character) || character == '_'; - } - } - - private class Visitor : RazorIRNodeVisitor - { - private readonly RazorIRBuilder _document; - private readonly RazorIRBuilder _namespace; - private readonly RazorIRBuilder _class; - private readonly RazorIRBuilder _method; - - public Visitor(RazorIRBuilder document, RazorIRBuilder @namespace, RazorIRBuilder @class, RazorIRBuilder method) - { - _document = document; - _namespace = @namespace; - _class = @class; - _method = method; - } - - public override void VisitChecksum(ChecksumIRNode node) - { - _document.Insert(0, node); - } - - public override void VisitUsingStatement(UsingStatementIRNode node) - { - _namespace.AddAfter(node); - } - - public override void VisitDeclareTagHelperFields(DeclareTagHelperFieldsIRNode node) - { - _class.Insert(0, node); - } - - public override void VisitDefault(RazorIRNode node) - { - _method.Add(node); - } - } + protected override string ClassifyDocument(RazorCodeDocument codeDocument, DocumentIRNode irDocument) + => DocumentKind; } } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Host/PageDirective.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Host/PageDirective.cs new file mode 100644 index 0000000000..341765715c --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Host/PageDirective.cs @@ -0,0 +1,48 @@ +// 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.Linq; +using Microsoft.AspNetCore.Razor.Evolution; +using Microsoft.AspNetCore.Razor.Evolution.Intermediate; + +namespace Microsoft.AspNetCore.Mvc.Razor.Host +{ + public static class PageDirective + { + public static readonly DirectiveDescriptor DirectiveDescriptor = DirectiveDescriptorBuilder.Create("page").AddString().Build(); + + public static IRazorEngineBuilder Register(IRazorEngineBuilder builder) + { + builder.AddDirective(DirectiveDescriptor); + return builder; + } + + public static bool TryGetRouteTemplate(DocumentIRNode irDocument, out string routeTemplate) + { + var visitor = new Visitor(); + for (var i = 0; i < irDocument.Children.Count; i++) + { + visitor.Visit(irDocument.Children[i]); + } + + routeTemplate = visitor.RouteTemplate; + return routeTemplate != null; + } + + private class Visitor : RazorIRNodeWalker + { + public DirectiveIRNode DirectiveNode { get; private set; } + + public string RouteTemplate { get; private set; } + + public override void VisitDirective(DirectiveIRNode node) + { + if (node.Descriptor == DirectiveDescriptor) + { + DirectiveNode = node; + RouteTemplate = node.Tokens.First().Content; + } + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Host/RazorPageDocumentClassifier.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Host/RazorPageDocumentClassifier.cs new file mode 100644 index 0000000000..c521bc05fc --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Host/RazorPageDocumentClassifier.cs @@ -0,0 +1,26 @@ +// 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.Razor.Evolution; +using Microsoft.AspNetCore.Razor.Evolution.Intermediate; + +namespace Microsoft.AspNetCore.Mvc.Razor.Host +{ + public class RazorPageDocumentClassifier : BaseDocumentClassifierPass + { + public static readonly string DocumentKind = "mvc.1.0.razor-page"; + + protected override string BaseType => "Microsoft.AspNetCore.Mvc.RazorPages.Page"; + + protected override string ClassifyDocument(RazorCodeDocument codeDocument, DocumentIRNode irDocument) + { + string routePrefix; + if (PageDirective.TryGetRouteTemplate(irDocument, out routePrefix)) + { + return DocumentKind; + } + + return null; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs index 998f9b1be5..0452fba843 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs @@ -183,9 +183,11 @@ namespace Microsoft.Extensions.DependencyInjection { InjectDirective.Register(b); ModelDirective.Register(b); + PageDirective.Register(b); b.Features.Add(new ModelExpressionPass()); b.Features.Add(new ViewComponentTagHelperPass()); + b.Features.Add(new RazorPageDocumentClassifier()); b.Features.Add(new MvcViewDocumentClassifierPass()); b.Features.Add(new DefaultInstrumentationPass()); diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorProject.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorProject.cs index 7afb4ba867..dd49bdfb95 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorProject.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorProject.cs @@ -49,7 +49,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal { if (file.IsDirectory) { - var children = EnumerateFiles(_provider.GetDirectoryContents(file.PhysicalPath), basePath, prefix + "/" + file.Name); + var relativePath = prefix + "/" + file.Name; + var subDirectory = _provider.GetDirectoryContents(relativePath); + var children = EnumerateFiles(subDirectory, basePath, relativePath); foreach (var child in children) { yield return child; diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorProjectItem.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorProjectItem.cs index e57b86e994..97526bc2b3 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorProjectItem.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorProjectItem.cs @@ -9,26 +9,26 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal { public class DefaultRazorProjectItem : RazorProjectItem { - private readonly IFileInfo _fileInfo; - public DefaultRazorProjectItem(IFileInfo fileInfo, string basePath, string path) { - _fileInfo = fileInfo; + FileInfo = fileInfo; BasePath = basePath; Path = path; } + public IFileInfo FileInfo { get; } + public override string BasePath { get; } public override string Path { get; } - public override bool Exists => _fileInfo.Exists; + public override bool Exists => FileInfo.Exists; - public override string PhysicalPath => _fileInfo.PhysicalPath; + public override string PhysicalPath => FileInfo.PhysicalPath; public override Stream Read() { - return _fileInfo.CreateReadStream(); + return FileInfo.CreateReadStream(); } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs index 7e26b659a5..87888c486a 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs @@ -17,7 +17,6 @@ using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; -using Microsoft.AspNetCore.Razor.Runtime.TagHelpers; using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.Extensions.DependencyInjection; @@ -26,15 +25,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor /// /// Represents properties and methods that are needed in order to render a view that uses Razor syntax. /// - public abstract class RazorPage : IRazorPage + public abstract class RazorPage : RazorPageBase, IRazorPage { private readonly HashSet _renderedSections = new HashSet(StringComparer.OrdinalIgnoreCase); private readonly Stack _tagHelperScopes = new Stack(); private IUrlHelper _urlHelper; private ITagHelperFactory _tagHelperFactory; private bool _renderedBody; - private AttributeInfo _attributeInfo; - private TagHelperAttributeInfo _tagHelperAttributeInfo; private StringWriter _valueBuffer; private IViewBufferScope _bufferScope; private bool _ignoreBody; @@ -118,8 +115,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor /// public IDictionary SectionWriters { get; } - /// - public abstract Task ExecuteAsync(); + protected override TextWriter Writer => Output; + + protected override HtmlEncoder Encoder => HtmlEncoder; private ITagHelperFactory TagHelperFactory { @@ -188,7 +186,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor /// non- C# expressions. If null, does not change . /// /// - /// All writes to the or after calling this method will + /// All writes to the or after calling this method will /// be buffered until is called. /// public void StartTagHelperWritingScope(HtmlEncoder encoder) @@ -235,7 +233,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor /// Starts a new scope for writing attribute values. /// /// - /// All writes to the or after calling this method will + /// All writes to the or after calling this method will /// be buffered until is called. /// The content will be buffered using a shared within this /// Nesting of and method calls @@ -286,368 +284,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor return content; } - /// - /// Writes the specified with HTML encoding to . - /// - /// The to write. - public virtual void Write(object value) - { - WriteTo(Output, value); - } - - /// - /// Writes the specified with HTML encoding to . - /// - /// The instance to write to. - /// The to write. - /// - /// s of type are written using - /// . - /// For all other types, the encoded result of is written to the - /// . - /// - public virtual void WriteTo(TextWriter writer, object value) - { - if (writer == null) - { - throw new ArgumentNullException(nameof(writer)); - } - - WriteTo(writer, HtmlEncoder, value); - } - - /// - /// Writes the specified with HTML encoding to given . - /// - /// The instance to write to. - /// - /// The to use when encoding . - /// - /// The to write. - /// - /// s of type are written using - /// . - /// For all other types, the encoded result of is written to the - /// . - /// - public static void WriteTo(TextWriter writer, HtmlEncoder encoder, object value) - { - if (writer == null) - { - throw new ArgumentNullException(nameof(writer)); - } - - if (encoder == null) - { - throw new ArgumentNullException(nameof(encoder)); - } - - if (value == null || value == HtmlString.Empty) - { - return; - } - - var htmlContent = value as IHtmlContent; - if (htmlContent != null) - { - var bufferedWriter = writer as ViewBufferTextWriter; - if (bufferedWriter == null || !bufferedWriter.IsBuffering) - { - htmlContent.WriteTo(writer, encoder); - } - else - { - var htmlContentContainer = value as IHtmlContentContainer; - if (htmlContentContainer != null) - { - // This is likely another ViewBuffer. - htmlContentContainer.MoveTo(bufferedWriter.Buffer); - } - else - { - // Perf: This is the common case for IHtmlContent, ViewBufferTextWriter is inefficient - // for writing character by character. - bufferedWriter.Buffer.AppendHtml(htmlContent); - } - } - - return; - } - - WriteTo(writer, encoder, value.ToString()); - } - - /// - /// Writes the specified with HTML encoding to . - /// - /// The instance to write to. - /// The to write. - public virtual void WriteTo(TextWriter writer, string value) - { - if (writer == null) - { - throw new ArgumentNullException(nameof(writer)); - } - - WriteTo(writer, HtmlEncoder, value); - } - - private static void WriteTo(TextWriter writer, HtmlEncoder encoder, string value) - { - if (!string.IsNullOrEmpty(value)) - { - // Perf: Encode right away instead of writing it character-by-character. - // character-by-character isn't efficient when using a writer backed by a ViewBuffer. - var encoded = encoder.Encode(value); - writer.Write(encoded); - } - } - - /// - /// Writes the specified without HTML encoding to . - /// - /// The to write. - public virtual void WriteLiteral(object value) - { - WriteLiteralTo(Output, value); - } - - /// - /// Writes the specified without HTML encoding to the . - /// - /// The instance to write to. - /// The to write. - public virtual void WriteLiteralTo(TextWriter writer, object value) - { - if (writer == null) - { - throw new ArgumentNullException(nameof(writer)); - } - - if (value != null) - { - WriteLiteralTo(writer, value.ToString()); - } - } - - /// - /// Writes the specified without HTML encoding to . - /// - /// The instance to write to. - /// The to write. - public virtual void WriteLiteralTo(TextWriter writer, string value) - { - if (writer == null) - { - throw new ArgumentNullException(nameof(writer)); - } - - if (!string.IsNullOrEmpty(value)) - { - writer.Write(value); - } - } - - public virtual void BeginWriteAttribute( - string name, - string prefix, - int prefixOffset, - string suffix, - int suffixOffset, - int attributeValuesCount) - { - BeginWriteAttributeTo(Output, name, prefix, prefixOffset, suffix, suffixOffset, attributeValuesCount); - } - - public virtual void BeginWriteAttributeTo( - TextWriter writer, - string name, - string prefix, - int prefixOffset, - string suffix, - int suffixOffset, - int attributeValuesCount) - { - if (writer == null) - { - throw new ArgumentNullException(nameof(writer)); - } - - if (prefix == null) - { - throw new ArgumentNullException(nameof(prefix)); - } - - if (suffix == null) - { - throw new ArgumentNullException(nameof(suffix)); - } - - _attributeInfo = new AttributeInfo(name, prefix, prefixOffset, suffix, suffixOffset, attributeValuesCount); - - // Single valued attributes might be omitted in entirety if it the attribute value strictly evaluates to - // null or false. Consequently defer the prefix generation until we encounter the attribute value. - if (attributeValuesCount != 1) - { - WritePositionTaggedLiteral(writer, prefix, prefixOffset); - } - } - - public void WriteAttributeValue( - string prefix, - int prefixOffset, - object value, - int valueOffset, - int valueLength, - bool isLiteral) - { - WriteAttributeValueTo(Output, prefix, prefixOffset, value, valueOffset, valueLength, isLiteral); - } - - public void WriteAttributeValueTo( - TextWriter writer, - string prefix, - int prefixOffset, - object value, - int valueOffset, - int valueLength, - bool isLiteral) - { - if (_attributeInfo.AttributeValuesCount == 1) - { - if (IsBoolFalseOrNullValue(prefix, value)) - { - // Value is either null or the bool 'false' with no prefix; don't render the attribute. - _attributeInfo.Suppressed = true; - return; - } - - // We are not omitting the attribute. Write the prefix. - WritePositionTaggedLiteral(writer, _attributeInfo.Prefix, _attributeInfo.PrefixOffset); - - if (IsBoolTrueWithEmptyPrefixValue(prefix, value)) - { - // The value is just the bool 'true', write the attribute name instead of the string 'True'. - value = _attributeInfo.Name; - } - } - - // This block handles two cases. - // 1. Single value with prefix. - // 2. Multiple values with or without prefix. - if (value != null) - { - if (!string.IsNullOrEmpty(prefix)) - { - WritePositionTaggedLiteral(writer, prefix, prefixOffset); - } - - BeginContext(valueOffset, valueLength, isLiteral); - - WriteUnprefixedAttributeValueTo(writer, value, isLiteral); - - EndContext(); - } - } - - public virtual void EndWriteAttribute() - { - EndWriteAttributeTo(Output); - } - - public virtual void EndWriteAttributeTo(TextWriter writer) - { - if (writer == null) - { - throw new ArgumentNullException(nameof(writer)); - } - - if (!_attributeInfo.Suppressed) - { - WritePositionTaggedLiteral(writer, _attributeInfo.Suffix, _attributeInfo.SuffixOffset); - } - } - - public void BeginAddHtmlAttributeValues( - TagHelperExecutionContext executionContext, - string attributeName, - int attributeValuesCount, - HtmlAttributeValueStyle attributeValueStyle) - { - _tagHelperAttributeInfo = new TagHelperAttributeInfo( - executionContext, - attributeName, - attributeValuesCount, - attributeValueStyle); - } - - public void AddHtmlAttributeValue( - string prefix, - int prefixOffset, - object value, - int valueOffset, - int valueLength, - bool isLiteral) - { - Debug.Assert(_tagHelperAttributeInfo.ExecutionContext != null); - if (_tagHelperAttributeInfo.AttributeValuesCount == 1) - { - if (IsBoolFalseOrNullValue(prefix, value)) - { - // The first value was 'null' or 'false' indicating that we shouldn't render the attribute. The - // attribute is treated as a TagHelper attribute so it's only available in - // TagHelperContext.AllAttributes for TagHelper authors to see (if they want to see why the - // attribute was removed from TagHelperOutput.Attributes). - _tagHelperAttributeInfo.ExecutionContext.AddTagHelperAttribute( - _tagHelperAttributeInfo.Name, - value?.ToString() ?? string.Empty, - _tagHelperAttributeInfo.AttributeValueStyle); - _tagHelperAttributeInfo.Suppressed = true; - return; - } - else if (IsBoolTrueWithEmptyPrefixValue(prefix, value)) - { - _tagHelperAttributeInfo.ExecutionContext.AddHtmlAttribute( - _tagHelperAttributeInfo.Name, - _tagHelperAttributeInfo.Name, - _tagHelperAttributeInfo.AttributeValueStyle); - _tagHelperAttributeInfo.Suppressed = true; - return; - } - } - - if (value != null) - { - // Perf: We'll use this buffer for all of the attribute values and then clear it to - // reduce allocations. - if (_valueBuffer == null) - { - _valueBuffer = new StringWriter(); - } - - if (!string.IsNullOrEmpty(prefix)) - { - WriteLiteralTo(_valueBuffer, prefix); - } - - WriteUnprefixedAttributeValueTo(_valueBuffer, value, isLiteral); - } - } - - public void EndAddHtmlAttributeValues(TagHelperExecutionContext executionContext) - { - if (!_tagHelperAttributeInfo.Suppressed) - { - // Perf: _valueBuffer might be null if nothing was written. If it is set, clear it so - // it is reset for the next value. - var content = _valueBuffer == null ? HtmlString.Empty : new HtmlString(_valueBuffer.ToString()); - _valueBuffer?.GetStringBuilder().Clear(); - - executionContext.AddHtmlAttribute(_tagHelperAttributeInfo.Name, content, _tagHelperAttributeInfo.AttributeValueStyle); - } - } - - public virtual string Href(string contentPath) + public override string Href(string contentPath) { if (contentPath == null) { @@ -664,36 +301,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor return _urlHelper.Content(contentPath); } - private void WriteUnprefixedAttributeValueTo(TextWriter writer, object value, bool isLiteral) - { - var stringValue = value as string; - - // The extra branching here is to ensure that we call the Write*To(string) overload where possible. - if (isLiteral && stringValue != null) - { - WriteLiteralTo(writer, stringValue); - } - else if (isLiteral) - { - WriteLiteralTo(writer, value); - } - else if (stringValue != null) - { - WriteTo(writer, stringValue); - } - else - { - WriteTo(writer, value); - } - } - - private void WritePositionTaggedLiteral(TextWriter writer, string value, int position) - { - BeginContext(position, value.Length, isLiteral: true); - WriteLiteralTo(writer, value); - EndContext(); - } - /// /// In a Razor layout page, renders the portion of a content page that is not within a named section. /// @@ -724,7 +331,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor /// /// The name of the section to create. /// The to execute when rendering the section. - public void DefineSection(string name, RenderAsyncDelegate section) + public override void DefineSection(string name, RenderAsyncDelegate section) { if (name == null) { @@ -764,7 +371,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor /// /// The name of the section to render. /// An empty . - /// The method writes to the and the value returned is a token + /// The method writes to the and the value returned is a token /// value that allows the Write (produced due to @RenderSection(..)) to succeed. However the /// value does not represent the rendered content. public HtmlString RenderSection(string name) @@ -783,7 +390,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor /// The section to render. /// Indicates if this section must be rendered. /// An empty . - /// The method writes to the and the value returned is a token + /// The method writes to the and the value returned is a token /// value that allows the Write (produced due to @RenderSection(..)) to succeed. However the /// value does not represent the rendered content. public HtmlString RenderSection(string name, bool required) @@ -806,7 +413,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor /// /// A that on completion returns an empty . /// - /// The method writes to the and the value returned is a token + /// The method writes to the and the value returned is a token /// value that allows the Write (produced due to @RenderSection(..)) to succeed. However the /// value does not represent the rendered content. public Task RenderSectionAsync(string name) @@ -828,7 +435,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor /// /// A that on completion returns an empty . /// - /// The method writes to the and the value returned is a token + /// The method writes to the and the value returned is a token /// value that allows the Write (produced due to @RenderSection(..)) to succeed. However the /// value does not represent the rendered content. /// if is true and the section @@ -908,7 +515,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor } /// - /// Invokes on and + /// Invokes on and /// on the response stream, writing out any buffered content to the . /// /// A that represents the asynchronous flush operation and on @@ -936,7 +543,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor throw new InvalidOperationException(message); } - await Output.FlushAsync(); + await Writer.FlushAsync(); await Context.Response.Body.FlushAsync(); return HtmlString.Empty; } @@ -977,7 +584,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor } } - public void BeginContext(int position, int length, bool isLiteral) + public override void BeginContext(int position, int length, bool isLiteral) { const string BeginContextEvent = "Microsoft.AspNetCore.Mvc.Razor.BeginInstrumentationContext"; @@ -996,7 +603,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor } } - public void EndContext() + public override void EndContext() { const string EndContextEvent = "Microsoft.AspNetCore.Mvc.Razor.EndInstrumentationContext"; @@ -1026,20 +633,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor return HtmlString.Empty; } - private bool IsBoolFalseOrNullValue(string prefix, object value) - { - return string.IsNullOrEmpty(prefix) && - (value == null || - (value is bool && !(bool)value)); - } - - private bool IsBoolTrueWithEmptyPrefixValue(string prefix, object value) - { - // If the value is just the bool 'true', use the attribute name as the value. - return string.IsNullOrEmpty(prefix) && - (value is bool && (bool)value); - } - private void EnsureMethodCanBeInvoked(string methodName) { if (PreviousSectionWriters == null) @@ -1048,68 +641,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor } } - private struct AttributeInfo - { - public AttributeInfo( - string name, - string prefix, - int prefixOffset, - string suffix, - int suffixOffset, - int attributeValuesCount) - { - Name = name; - Prefix = prefix; - PrefixOffset = prefixOffset; - Suffix = suffix; - SuffixOffset = suffixOffset; - AttributeValuesCount = attributeValuesCount; - - Suppressed = false; - } - - public int AttributeValuesCount { get; } - - public string Name { get; } - - public string Prefix { get; } - - public int PrefixOffset { get; } - - public string Suffix { get; } - - public int SuffixOffset { get; } - - public bool Suppressed { get; set; } - } - - private struct TagHelperAttributeInfo - { - public TagHelperAttributeInfo( - TagHelperExecutionContext tagHelperExecutionContext, - string name, - int attributeValuesCount, - HtmlAttributeValueStyle attributeValueStyle) - { - ExecutionContext = tagHelperExecutionContext; - Name = name; - AttributeValuesCount = attributeValuesCount; - AttributeValueStyle = attributeValueStyle; - - Suppressed = false; - } - - public string Name { get; } - - public TagHelperExecutionContext ExecutionContext { get; } - - public int AttributeValuesCount { get; } - - public HtmlAttributeValueStyle AttributeValueStyle { get; } - - public bool Suppressed { get; set; } - } - private struct TagHelperScopeInfo { public TagHelperScopeInfo(ViewBuffer buffer, HtmlEncoder encoder, TextWriter writer) diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPageBase.cs b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPageBase.cs new file mode 100644 index 0000000000..05f3e8ca16 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPageBase.cs @@ -0,0 +1,514 @@ +// 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.Diagnostics; +using System.IO; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; +using Microsoft.AspNetCore.Razor.Runtime.TagHelpers; +using Microsoft.AspNetCore.Razor.TagHelpers; + +namespace Microsoft.AspNetCore.Mvc.Razor +{ + /// + /// Represents properties and methods that are needed in order to render a view that uses Razor syntax. + /// + public abstract class RazorPageBase + { + private AttributeInfo _attributeInfo; + private TagHelperAttributeInfo _tagHelperAttributeInfo; + private StringWriter _valueBuffer; + + /// + /// Gets the that the page is writing output to. + /// + protected abstract TextWriter Writer { get; } + + protected abstract HtmlEncoder Encoder { get; } + + public abstract Task ExecuteAsync(); + + /// + /// Writes the specified with HTML encoding to . + /// + /// The to write. + public virtual void Write(object value) + { + WriteTo(Writer, value); + } + + /// + /// Writes the specified with HTML encoding to . + /// + /// The instance to write to. + /// The to write. + /// + /// s of type are written using + /// . + /// For all other types, the encoded result of is written to the + /// . + /// + public virtual void WriteTo(TextWriter writer, object value) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + WriteTo(writer, Encoder, value); + } + + /// + /// Writes the specified with HTML encoding to given . + /// + /// The instance to write to. + /// + /// The to use when encoding . + /// + /// The to write. + /// + /// s of type are written using + /// . + /// For all other types, the encoded result of is written to the + /// . + /// + public static void WriteTo(TextWriter writer, HtmlEncoder encoder, object value) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (encoder == null) + { + throw new ArgumentNullException(nameof(encoder)); + } + + if (value == null || value == HtmlString.Empty) + { + return; + } + + var htmlContent = value as IHtmlContent; + if (htmlContent != null) + { + var bufferedWriter = writer as ViewBufferTextWriter; + if (bufferedWriter == null || !bufferedWriter.IsBuffering) + { + htmlContent.WriteTo(writer, encoder); + } + else + { + var htmlContentContainer = value as IHtmlContentContainer; + if (htmlContentContainer != null) + { + // This is likely another ViewBuffer. + htmlContentContainer.MoveTo(bufferedWriter.Buffer); + } + else + { + // Perf: This is the common case for IHtmlContent, ViewBufferTextWriter is inefficient + // for writing character by character. + bufferedWriter.Buffer.AppendHtml(htmlContent); + } + } + + return; + } + + WriteTo(writer, encoder, value.ToString()); + } + + /// + /// Writes the specified with HTML encoding to . + /// + /// The instance to write to. + /// The to write. + public virtual void WriteTo(TextWriter writer, string value) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + WriteTo(writer, Encoder, value); + } + + private static void WriteTo(TextWriter writer, HtmlEncoder encoder, string value) + { + if (!string.IsNullOrEmpty(value)) + { + // Perf: Encode right away instead of writing it character-by-character. + // character-by-character isn't efficient when using a writer backed by a ViewBuffer. + var encoded = encoder.Encode(value); + writer.Write(encoded); + } + } + + /// + /// Writes the specified without HTML encoding to . + /// + /// The to write. + public virtual void WriteLiteral(object value) + { + WriteLiteralTo(Writer, value); + } + + /// + /// Writes the specified without HTML encoding to the . + /// + /// The instance to write to. + /// The to write. + public virtual void WriteLiteralTo(TextWriter writer, object value) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (value != null) + { + WriteLiteralTo(writer, value.ToString()); + } + } + + /// + /// Writes the specified without HTML encoding to . + /// + /// The instance to write to. + /// The to write. + public virtual void WriteLiteralTo(TextWriter writer, string value) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (!string.IsNullOrEmpty(value)) + { + writer.Write(value); + } + } + + public virtual void BeginWriteAttribute( + string name, + string prefix, + int prefixOffset, + string suffix, + int suffixOffset, + int attributeValuesCount) + { + BeginWriteAttributeTo(Writer, name, prefix, prefixOffset, suffix, suffixOffset, attributeValuesCount); + } + + public virtual void BeginWriteAttributeTo( + TextWriter writer, + string name, + string prefix, + int prefixOffset, + string suffix, + int suffixOffset, + int attributeValuesCount) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (prefix == null) + { + throw new ArgumentNullException(nameof(prefix)); + } + + if (suffix == null) + { + throw new ArgumentNullException(nameof(suffix)); + } + + _attributeInfo = new AttributeInfo(name, prefix, prefixOffset, suffix, suffixOffset, attributeValuesCount); + + // Single valued attributes might be omitted in entirety if it the attribute value strictly evaluates to + // null or false. Consequently defer the prefix generation until we encounter the attribute value. + if (attributeValuesCount != 1) + { + WritePositionTaggedLiteral(writer, prefix, prefixOffset); + } + } + + public void WriteAttributeValue( + string prefix, + int prefixOffset, + object value, + int valueOffset, + int valueLength, + bool isLiteral) + { + WriteAttributeValueTo(Writer, prefix, prefixOffset, value, valueOffset, valueLength, isLiteral); + } + + public void WriteAttributeValueTo( + TextWriter writer, + string prefix, + int prefixOffset, + object value, + int valueOffset, + int valueLength, + bool isLiteral) + { + if (_attributeInfo.AttributeValuesCount == 1) + { + if (IsBoolFalseOrNullValue(prefix, value)) + { + // Value is either null or the bool 'false' with no prefix; don't render the attribute. + _attributeInfo.Suppressed = true; + return; + } + + // We are not omitting the attribute. Write the prefix. + WritePositionTaggedLiteral(writer, _attributeInfo.Prefix, _attributeInfo.PrefixOffset); + + if (IsBoolTrueWithEmptyPrefixValue(prefix, value)) + { + // The value is just the bool 'true', write the attribute name instead of the string 'True'. + value = _attributeInfo.Name; + } + } + + // This block handles two cases. + // 1. Single value with prefix. + // 2. Multiple values with or without prefix. + if (value != null) + { + if (!string.IsNullOrEmpty(prefix)) + { + WritePositionTaggedLiteral(writer, prefix, prefixOffset); + } + + BeginContext(valueOffset, valueLength, isLiteral); + + WriteUnprefixedAttributeValueTo(writer, value, isLiteral); + + EndContext(); + } + } + + public virtual void EndWriteAttribute() + { + EndWriteAttributeTo(Writer); + } + + public virtual void EndWriteAttributeTo(TextWriter writer) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (!_attributeInfo.Suppressed) + { + WritePositionTaggedLiteral(writer, _attributeInfo.Suffix, _attributeInfo.SuffixOffset); + } + } + + public void BeginAddHtmlAttributeValues( + TagHelperExecutionContext executionContext, + string attributeName, + int attributeValuesCount, + HtmlAttributeValueStyle attributeValueStyle) + { + _tagHelperAttributeInfo = new TagHelperAttributeInfo( + executionContext, + attributeName, + attributeValuesCount, + attributeValueStyle); + } + + public void AddHtmlAttributeValue( + string prefix, + int prefixOffset, + object value, + int valueOffset, + int valueLength, + bool isLiteral) + { + Debug.Assert(_tagHelperAttributeInfo.ExecutionContext != null); + if (_tagHelperAttributeInfo.AttributeValuesCount == 1) + { + if (IsBoolFalseOrNullValue(prefix, value)) + { + // The first value was 'null' or 'false' indicating that we shouldn't render the attribute. The + // attribute is treated as a TagHelper attribute so it's only available in + // TagHelperContext.AllAttributes for TagHelper authors to see (if they want to see why the + // attribute was removed from TagHelperOutput.Attributes). + _tagHelperAttributeInfo.ExecutionContext.AddTagHelperAttribute( + _tagHelperAttributeInfo.Name, + value?.ToString() ?? string.Empty, + _tagHelperAttributeInfo.AttributeValueStyle); + _tagHelperAttributeInfo.Suppressed = true; + return; + } + else if (IsBoolTrueWithEmptyPrefixValue(prefix, value)) + { + _tagHelperAttributeInfo.ExecutionContext.AddHtmlAttribute( + _tagHelperAttributeInfo.Name, + _tagHelperAttributeInfo.Name, + _tagHelperAttributeInfo.AttributeValueStyle); + _tagHelperAttributeInfo.Suppressed = true; + return; + } + } + + if (value != null) + { + // Perf: We'll use this buffer for all of the attribute values and then clear it to + // reduce allocations. + if (_valueBuffer == null) + { + _valueBuffer = new StringWriter(); + } + + if (!string.IsNullOrEmpty(prefix)) + { + WriteLiteralTo(_valueBuffer, prefix); + } + + WriteUnprefixedAttributeValueTo(_valueBuffer, value, isLiteral); + } + } + + public void EndAddHtmlAttributeValues(TagHelperExecutionContext executionContext) + { + if (!_tagHelperAttributeInfo.Suppressed) + { + // Perf: _valueBuffer might be null if nothing was written. If it is set, clear it so + // it is reset for the next value. + var content = _valueBuffer == null ? HtmlString.Empty : new HtmlString(_valueBuffer.ToString()); + _valueBuffer?.GetStringBuilder().Clear(); + + executionContext.AddHtmlAttribute(_tagHelperAttributeInfo.Name, content, _tagHelperAttributeInfo.AttributeValueStyle); + } + } + + public abstract string Href(string contentPath); + + private void WriteUnprefixedAttributeValueTo(TextWriter writer, object value, bool isLiteral) + { + var stringValue = value as string; + + // The extra branching here is to ensure that we call the Write*To(string) overload where possible. + if (isLiteral && stringValue != null) + { + WriteLiteralTo(writer, stringValue); + } + else if (isLiteral) + { + WriteLiteralTo(writer, value); + } + else if (stringValue != null) + { + WriteTo(writer, stringValue); + } + else + { + WriteTo(writer, value); + } + } + + private void WritePositionTaggedLiteral(TextWriter writer, string value, int position) + { + BeginContext(position, value.Length, isLiteral: true); + WriteLiteralTo(writer, value); + EndContext(); + } + + /// + /// Creates a named content section in the page. + /// + /// The name of the section to create. + /// The to execute when rendering the section. + public abstract void DefineSection(string name, RenderAsyncDelegate section); + + public abstract void BeginContext(int position, int length, bool isLiteral); + + public abstract void EndContext(); + + private bool IsBoolFalseOrNullValue(string prefix, object value) + { + return string.IsNullOrEmpty(prefix) && + (value == null || + (value is bool && !(bool)value)); + } + + private bool IsBoolTrueWithEmptyPrefixValue(string prefix, object value) + { + // If the value is just the bool 'true', use the attribute name as the value. + return string.IsNullOrEmpty(prefix) && + (value is bool && (bool)value); + } + + private struct AttributeInfo + { + public AttributeInfo( + string name, + string prefix, + int prefixOffset, + string suffix, + int suffixOffset, + int attributeValuesCount) + { + Name = name; + Prefix = prefix; + PrefixOffset = prefixOffset; + Suffix = suffix; + SuffixOffset = suffixOffset; + AttributeValuesCount = attributeValuesCount; + + Suppressed = false; + } + + public int AttributeValuesCount { get; } + + public string Name { get; } + + public string Prefix { get; } + + public int PrefixOffset { get; } + + public string Suffix { get; } + + public int SuffixOffset { get; } + + public bool Suppressed { get; set; } + } + + private struct TagHelperAttributeInfo + { + public TagHelperAttributeInfo( + TagHelperExecutionContext tagHelperExecutionContext, + string name, + int attributeValuesCount, + HtmlAttributeValueStyle attributeValueStyle) + { + ExecutionContext = tagHelperExecutionContext; + Name = name; + AttributeValuesCount = attributeValuesCount; + AttributeValueStyle = attributeValueStyle; + + Suppressed = false; + } + + public string Name { get; } + + public TagHelperExecutionContext ExecutionContext { get; } + + public int AttributeValuesCount { get; } + + public HtmlAttributeValueStyle AttributeValueStyle { get; } + + public bool Suppressed { get; set; } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs new file mode 100644 index 0000000000..7c2da38b2b --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs @@ -0,0 +1,71 @@ +// 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.Razor; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; +using Microsoft.AspNetCore.Mvc.RazorPages.Internal; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class MvcRazorPagesMvcCoreBuilderExtensions + { + public static IMvcCoreBuilder AddRazorPages(this IMvcCoreBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.AddRazorViewEngine(); + AddServices(builder.Services); + return builder; + } + + public static IMvcCoreBuilder AddRazorPages( + this IMvcCoreBuilder builder, + Action setupAction) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (setupAction == null) + { + throw new ArgumentNullException(nameof(setupAction)); + } + + builder.AddRazorViewEngine(); + AddServices(builder.Services); + + builder.Services.Configure(setupAction); + + return builder; + } + + // Internal for testing. + internal static void AddServices(IServiceCollection services) + { + services.TryAddEnumerable( + ServiceDescriptor.Singleton()); + + services.TryAddEnumerable( + ServiceDescriptor.Singleton()); + + services.TryAddSingleton(); + services.TryAddSingleton(); + + services.TryAddSingleton(); + services.TryAddSingleton(); + + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs index e1ff6a6e45..a64ee5cdf7 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs @@ -71,7 +71,17 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure if (string.Equals(IndexFileName, item.Filename, StringComparison.OrdinalIgnoreCase)) { - model.Selectors.Add(CreateSelectorModel(item.BasePath, template)); + var parentDirectoryPath = item.Path; + var index = parentDirectoryPath.LastIndexOf('/'); + if (index == -1) + { + parentDirectoryPath = string.Empty; + } + else + { + parentDirectoryPath = parentDirectoryPath.Substring(0, index); + } + model.Selectors.Add(CreateSelectorModel(parentDirectoryPath, template)); } for (var i = 0; i < _pagesOptions.Conventions.Count; i++) diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageResultExecutor.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageResultExecutor.cs index 65aba3b9ab..64d088f2a9 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageResultExecutor.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageResultExecutor.cs @@ -6,6 +6,7 @@ using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Razor; +using Microsoft.AspNetCore.Mvc.RazorPages.Internal; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewFeatures; @@ -39,7 +40,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure : base(writerFactory, compositeViewEngine, diagnosticSource) { _razorViewEngine = razorViewEngine; - _razorPageActivator = razorPageActivator; + _razorPageActivator = new PassThruRazorPageActivator(razorPageActivator); _htmlEncoder = htmlEncoder; } @@ -54,6 +55,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure } var view = new RazorView(_razorViewEngine, _razorPageActivator, pageContext.PageStarts, result.Page, _htmlEncoder); + pageContext.View = view; return ExecuteAsync(pageContext, result.ContentType, result.StatusCode); } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageHandlerMethodSelector.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageHandlerMethodSelector.cs new file mode 100644 index 0000000000..a53b713aad --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageHandlerMethodSelector.cs @@ -0,0 +1,13 @@ +using System; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public class DefaultPageHandlerMethodSelector : IPageHandlerMethodSelector + { + public HandlerMethodDescriptor Select(PageContext context) + { + return null; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageLoader.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageLoader.cs new file mode 100644 index 0000000000..d70c00f85e --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageLoader.cs @@ -0,0 +1,40 @@ +// 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.Razor.Compilation; +using Microsoft.AspNetCore.Mvc.Razor.Internal; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; +using Microsoft.AspNetCore.Razor.Evolution; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public class DefaultPageLoader : IPageLoader + { + private readonly IRazorCompilationService _razorCompilationService; + private readonly RazorProject _project; + + public DefaultPageLoader( + IRazorCompilationService razorCompilationService, + RazorProject razorProject) + { + _razorCompilationService = razorCompilationService; + _project = razorProject; + } + + public Type Load(PageActionDescriptor actionDescriptor) + { + var item = _project.GetItem(actionDescriptor.RelativePath); + if (!item.Exists) + { + throw new InvalidOperationException($"File {actionDescriptor.RelativePath} was not found."); + } + + var projectItem = (DefaultRazorProjectItem)item; + var compilationResult = _razorCompilationService.Compile(new RelativeFileInfo(projectItem.FileInfo, item.Path)); + compilationResult.EnsureSuccessful(); + + return compilationResult.CompiledType; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs index f2988fc103..49a479077f 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs @@ -145,6 +145,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal tempData, _htmlHelperOptions); + pageContext.ActionDescriptor = cacheEntry.ActionDescriptor; + return new PageActionInvoker( _selector, _diagnosticSource, diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PassThruRazorPageActivator.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PassThruRazorPageActivator.cs new file mode 100644 index 0000000000..3da7fd9897 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PassThruRazorPageActivator.cs @@ -0,0 +1,29 @@ +// 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.Razor; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public class PassThruRazorPageActivator : IRazorPageActivator + { + private readonly IRazorPageActivator _pageActivator; + + public PassThruRazorPageActivator(IRazorPageActivator pageActivator) + { + _pageActivator = pageActivator; + } + + public void Activate(IRazorPage page, ViewContext context) + { + var razorView = (RazorView)context.View; + if (ReferenceEquals(page, razorView.RazorPage)) + { + return; + } + + _pageActivator.Activate(page, context); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Page.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Page.cs index a1bf56d1a2..bc926861a0 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Page.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Page.cs @@ -3,18 +3,25 @@ using System; using System.Collections.Generic; -using System.Threading.Tasks; +using System.Diagnostics; +using System.IO; +using System.Text.Encodings.Web; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc.Razor; +using Microsoft.AspNetCore.Mvc.Razor.Internal; using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Mvc.RazorPages { /// /// A base class for a Razor page. /// - public abstract class Page : IRazorPage + public abstract class Page : RazorPageBase, IRazorPage { + private IUrlHelper _urlHelper; + /// public IHtmlContent BodyContent { get; set; } @@ -41,13 +48,84 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages /// public ViewContext ViewContext { get; set; } + /// + /// Gets or sets a instance used to instrument the page execution. + /// + [RazorInject] + public DiagnosticSource DiagnosticSource { get; set; } + + /// + /// Gets the to use when this + /// handles non- C# expressions. + /// + [RazorInject] + public HtmlEncoder HtmlEncoder { get; set; } + + protected override HtmlEncoder Encoder => HtmlEncoder; + + protected override TextWriter Writer => ViewContext.Writer; + /// public void EnsureRenderedBodyOrSections() { throw new NotImplementedException(); } - /// - public abstract Task ExecuteAsync(); + public override void BeginContext(int position, int length, bool isLiteral) + { + const string BeginContextEvent = "Microsoft.AspNetCore.Mvc.Razor.BeginInstrumentationContext"; + + if (DiagnosticSource?.IsEnabled(BeginContextEvent) == true) + { + DiagnosticSource.Write( + BeginContextEvent, + new + { + httpContext = ViewContext, + path = Path, + position = position, + length = length, + isLiteral = isLiteral, + }); + } + } + + public override void EndContext() + { + const string EndContextEvent = "Microsoft.AspNetCore.Mvc.Razor.EndInstrumentationContext"; + + if (DiagnosticSource?.IsEnabled(EndContextEvent) == true) + { + DiagnosticSource.Write( + EndContextEvent, + new + { + httpContext = ViewContext, + path = Path, + }); + } + } + + public override string Href(string contentPath) + { + if (contentPath == null) + { + throw new ArgumentNullException(nameof(contentPath)); + } + + if (_urlHelper == null) + { + var services = ViewContext.HttpContext.RequestServices; + var factory = services.GetRequiredService(); + _urlHelper = factory.GetUrlHelper(ViewContext); + } + + return _urlHelper.Content(contentPath); + } + + public override void DefineSection(string name, RenderAsyncDelegate section) + { + + } } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/project.json b/src/Microsoft.AspNetCore.Mvc.RazorPages/project.json index f0f67bb1fa..87972225b5 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/project.json +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/project.json @@ -25,7 +25,6 @@ "Microsoft.AspNetCore.Mvc.Razor": { "target": "project" }, - "Microsoft.AspNetCore.Razor.Evolution": "1.2.0-*", "Microsoft.Extensions.PropertyActivator.Sources": { "version": "1.2.0-*", "type": "build" diff --git a/src/Microsoft.AspNetCore.Mvc/MvcServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Mvc/MvcServiceCollectionExtensions.cs index 74e16726af..b7baee8634 100644 --- a/src/Microsoft.AspNetCore.Mvc/MvcServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc/MvcServiceCollectionExtensions.cs @@ -42,6 +42,7 @@ namespace Microsoft.Extensions.DependencyInjection builder.AddFormatterMappings(); builder.AddViews(); builder.AddRazorViewEngine(); + builder.AddRazorPages(); builder.AddCacheTagHelper(); // +1 order diff --git a/src/Microsoft.AspNetCore.Mvc/project.json b/src/Microsoft.AspNetCore.Mvc/project.json index f1075d689e..199bd865f4 100644 --- a/src/Microsoft.AspNetCore.Mvc/project.json +++ b/src/Microsoft.AspNetCore.Mvc/project.json @@ -35,7 +35,7 @@ "Microsoft.AspNetCore.Mvc.Localization": { "target": "project" }, - "Microsoft.AspNetCore.Mvc.Razor": { + "Microsoft.AspNetCore.Mvc.RazorPages": { "target": "project" }, "Microsoft.AspNetCore.Mvc.TagHelpers": { diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/MvcSandboxTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/MvcSandboxTest.cs index cf17224c9a..5b5b2f1196 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/MvcSandboxTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/MvcSandboxTest.cs @@ -25,5 +25,14 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests // Assert Assert.Contains("This sandbox should give you a quick view of a basic MVC application.", response); } + [Fact] + public async Task RazorPages_ReturnSuccess() + { + // Arrange & Act + var response = await Client.GetStringAsync("http://localhost/Pages/Test"); + + // Assert + Assert.Contains("This file should give you a quick view of a Mvc Raor Page in action.", response); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageTest.cs index cb076afc5c..24b4d478f3 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageTest.cs @@ -1518,7 +1518,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor { get { - var bufferedWriter = Assert.IsType(Output); + var bufferedWriter = Assert.IsType(Writer); using (var stringWriter = new StringWriter()) { bufferedWriter.Buffer.WriteTo(stringWriter, HtmlEncoder); diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/DefaultPageFactoryTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/DefaultPageFactoryTest.cs index b54fd794a1..736bcc78ae 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/DefaultPageFactoryTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/DefaultPageFactoryTest.cs @@ -244,7 +244,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure public IUrlHelper UrlHelper { get; set; } [RazorInject] - public HtmlEncoder HtmlEncoder { get; set; } + public new HtmlEncoder HtmlEncoder { get; set; } [RazorInject] public ViewDataDictionary ViewData { get; set; } diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/PageActionDescriptorProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/PageActionDescriptorProviderTest.cs index 7b6362903e..91b75d9a6e 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/PageActionDescriptorProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/PageActionDescriptorProviderTest.cs @@ -3,15 +3,14 @@ using System; using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Razor; -using Microsoft.AspNetCore.Mvc.ApplicationModels; -using Microsoft.AspNetCore.Mvc.RazorPages.Internal; +using Microsoft.AspNetCore.Mvc.Razor.Internal; +using Microsoft.AspNetCore.Razor.Evolution; using Microsoft.Extensions.Options; using Moq; using Xunit; -using Microsoft.AspNetCore.Razor.Evolution; -using Microsoft.AspNetCore.Mvc.Razor.Internal; namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { @@ -128,7 +127,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure razorProject.Setup(p => p.EnumerateItems("/")) .Returns(new[] { - GetProjectItem("/About", "/Index.cshtml", $"@page {Environment.NewLine}"), + GetProjectItem("", "/About/Index.cshtml", $"@page {Environment.NewLine}"), }); var provider = new PageActionDescriptorProvider( razorProject.Object, @@ -145,14 +144,14 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { var descriptor = Assert.IsType(result); Assert.Equal("/About/Index.cshtml", descriptor.RelativePath); - Assert.Equal("/Index", descriptor.RouteValues["page"]); + Assert.Equal("/About/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/Index", descriptor.RouteValues["page"]); Assert.Equal("About", descriptor.AttributeRouteInfo.Template); }); } @@ -165,7 +164,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure razorProject.Setup(p => p.EnumerateItems("/")) .Returns(new[] { - GetProjectItem("/Catalog/Details", "/Index.cshtml", $"@page {{id:int?}} {Environment.NewLine}"), + GetProjectItem("", "/Catalog/Details/Index.cshtml", $"@page {{id:int?}} {Environment.NewLine}"), }); var provider = new PageActionDescriptorProvider( razorProject.Object, @@ -182,14 +181,14 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { var descriptor = Assert.IsType(result); Assert.Equal("/Catalog/Details/Index.cshtml", descriptor.RelativePath); - Assert.Equal("/Index", descriptor.RouteValues["page"]); + Assert.Equal("/Catalog/Details/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/Index", descriptor.RouteValues["page"]); Assert.Equal("Catalog/Details/{id:int?}", descriptor.AttributeRouteInfo.Template); }); } diff --git a/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs index 9ab606e2a1..968d9957d8 100644 --- a/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs @@ -23,6 +23,8 @@ using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.Razor.Compilation; using Microsoft.AspNetCore.Mvc.Razor.Internal; using Microsoft.AspNetCore.Mvc.Razor.TagHelpers; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; +using Microsoft.AspNetCore.Mvc.RazorPages.Internal; using Microsoft.AspNetCore.Mvc.TagHelpers; using Microsoft.AspNetCore.Mvc.ViewComponents; using Microsoft.AspNetCore.Mvc.ViewFeatures; @@ -374,6 +376,7 @@ namespace Microsoft.AspNetCore.Mvc new Type[] { typeof(ControllerActionDescriptorProvider), + typeof(PageActionDescriptorProvider), } }, { @@ -381,6 +384,7 @@ namespace Microsoft.AspNetCore.Mvc new Type[] { typeof(ControllerActionInvokerProvider), + typeof(PageActionInvokerProvider), } }, { diff --git a/test/WebSites/RazorWebSite/Services/UpdateableFileProvider.cs b/test/WebSites/RazorWebSite/Services/UpdateableFileProvider.cs index 394230d275..0e3ef8aaac 100644 --- a/test/WebSites/RazorWebSite/Services/UpdateableFileProvider.cs +++ b/test/WebSites/RazorWebSite/Services/UpdateableFileProvider.cs @@ -25,10 +25,7 @@ namespace RazorWebSite }, }; - public IDirectoryContents GetDirectoryContents(string subpath) - { - throw new NotImplementedException(); - } + public IDirectoryContents GetDirectoryContents(string subpath) => new NotFoundDirectoryContents(); public void UpdateContent(string subpath, string content) {