diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/IMvcRazorHost.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/IMvcRazorHost.cs index 1011b242dc..79a0b2d700 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/IMvcRazorHost.cs +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/IMvcRazorHost.cs @@ -6,8 +6,24 @@ using Microsoft.AspNet.Razor; namespace Microsoft.AspNet.Mvc.Razor { + /// + /// Specifies the contracts for a Razor host that parses Razor files and generates C# code. + /// public interface IMvcRazorHost { + /// + /// Flag that indicates if page execution instrumentation code should be injected into the output. + /// + bool EnableInstrumentation { get; set; } + + /// + /// Parses and generates the contents of a Razor file represented by . + /// + /// The path of the relative to the root of the application. + /// Used to generate line pragmas and calculate the class name of the generated type. + /// A that represents the Razor contents. + /// A instance that represents the results of code generation. + /// GeneratorResults GenerateCode(string rootRelativePath, Stream inputStream); /// diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs index 0c54131bbe..598cf8cfd7 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs @@ -82,7 +82,9 @@ namespace Microsoft.AspNet.Mvc.Razor templateTypeName: "Microsoft.AspNet.Mvc.Razor.HelperResult", defineSectionMethodName: "DefineSection") { - ResolveUrlMethodName = "Href" + ResolveUrlMethodName = "Href", + BeginContextMethodName = "BeginContext", + EndContextMethodName = "EndContext" }; foreach (var ns in _defaultNamespaces) diff --git a/src/Microsoft.AspNet.Mvc.Razor/IRazorPage.cs b/src/Microsoft.AspNet.Mvc.Razor/IRazorPage.cs index f79fc0b351..4b3bdbd373 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/IRazorPage.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/IRazorPage.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; +using Microsoft.AspNet.PageExecutionInstrumentation; namespace Microsoft.AspNet.Mvc.Razor { @@ -44,6 +45,11 @@ namespace Microsoft.AspNet.Mvc.Razor /// string Layout { get; set; } + /// + /// Gets or sets a instance used to instrument the page execution. + /// + IPageExecutionContext PageExecutionContext { get; set; } + /// /// Gets or sets the sections that can be rendered by this page. /// diff --git a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs index 1d07b517ca..b897a77c8f 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs @@ -58,6 +58,22 @@ namespace Microsoft.AspNet.Mvc.Razor return string.Format(CultureInfo.CurrentCulture, GetString("FlushPointCannotBeInvoked"), p0); } + /// + /// The {0} returned by '{1}' must be an instance of '{2}'. + /// + internal static string Instrumentation_WriterMustBeBufferedTextWriter + { + get { return GetString("Instrumentation_WriterMustBeBufferedTextWriter"); } + } + + /// + /// The {0} returned by '{1}' must be an instance of '{2}'. + /// + internal static string FormatInstrumentation_WriterMustBeBufferedTextWriter(object p0, object p1, object p2) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Instrumentation_WriterMustBeBufferedTextWriter"), p0, p1, p2); + } + /// /// The layout view '{0}' could not be located. /// diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs index a1bddb1057..4e6abbcf78 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs @@ -10,6 +10,7 @@ using System.Security.Principal; using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.AspNet.PageExecutionInstrumentation; using Microsoft.Framework.DependencyInjection; namespace Microsoft.AspNet.Mvc.Razor @@ -47,8 +48,12 @@ namespace Microsoft.AspNet.Mvc.Razor /// public ViewContext ViewContext { get; set; } + /// public string Layout { get; set; } + /// + public IPageExecutionContext PageExecutionContext { get; set; } + /// /// Gets the TextWriter that the page is writing output to. /// @@ -268,6 +273,7 @@ namespace Microsoft.AspNet.Mvc.Razor // Calculate length of the source span by the position of the next value (or suffix) var sourceLength = next.Position - attrVal.Value.Position; + BeginContext(attrVal.Value.Position, sourceLength, isLiteral: attrVal.Literal); // The extra branching here is to ensure that we call the Write*To(string) overload whe // possible. if (attrVal.Literal && stringValue != null) @@ -287,6 +293,7 @@ namespace Microsoft.AspNet.Mvc.Razor WriteTo(writer, val.Value); } + EndContext(); wroteSomething = true; } if (wroteSomething) @@ -308,7 +315,9 @@ namespace Microsoft.AspNet.Mvc.Razor private void WritePositionTaggedLiteral(TextWriter writer, string value, int position) { + BeginContext(position, value.Length, isLiteral: true); WriteLiteralTo(writer, value); + EndContext(); } private void WritePositionTaggedLiteral(TextWriter writer, PositionTagged value) @@ -420,6 +429,16 @@ namespace Microsoft.AspNet.Mvc.Razor } } + public void BeginContext(int position, int length, bool isLiteral) + { + PageExecutionContext?.BeginContext(position, length, isLiteral); + } + + public void EndContext() + { + PageExecutionContext?.EndContext(); + } + private void EnsureMethodCanBeInvoked(string methodName) { if (PreviousSectionWriters == null) diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorTextWriter.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorTextWriter.cs index ba06edc82f..9ea894453c 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorTextWriter.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorTextWriter.cs @@ -18,7 +18,7 @@ namespace Microsoft.AspNet.Mvc.Razor /// This is primarily designed to avoid creating large in-memory strings. /// Refer to https://aspnetwebstack.codeplex.com/workitem/585 for more details. /// - public class RazorTextWriter : TextWriter + public class RazorTextWriter : TextWriter, IBufferedTextWriter { private static readonly Task _completedTask = Task.FromResult(0); private readonly TextWriter _unbufferedWriter; @@ -43,9 +43,7 @@ namespace Microsoft.AspNet.Mvc.Razor get { return _encoding; } } - /// - /// Gets a flag that determines if this instance of is buffering content. - /// + /// public bool IsBuffering { get; private set; } = true; /// @@ -267,10 +265,7 @@ namespace Microsoft.AspNet.Mvc.Razor await _unbufferedWriter.FlushAsync(); } - /// - /// Copies the content of the to the instance. - /// - /// The writer to copy contents to. + /// public void CopyTo(TextWriter writer) { var targetRazorTextWriter = writer as RazorTextWriter; @@ -286,11 +281,7 @@ namespace Microsoft.AspNet.Mvc.Razor } } - /// - /// Copies the content of the to the specified instance. - /// - /// The writer to copy contents to. - /// A task that represents the asynchronous copy operation. + /// public Task CopyToAsync(TextWriter writer) { var targetRazorTextWriter = writer as RazorTextWriter; diff --git a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx index 9d0f92e270..e2cfc45b7b 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx @@ -126,6 +126,9 @@ '{0}' cannot be invoked when a Layout page is set to be executed. + + The {0} returned by '{1}' must be an instance of '{2}'. + The layout view '{0}' could not be located. diff --git a/src/Microsoft.AspNet.Mvc.Razor/Services/IBufferedTextWriter.cs b/src/Microsoft.AspNet.Mvc.Razor/Services/IBufferedTextWriter.cs new file mode 100644 index 0000000000..c0fbae88eb --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/Services/IBufferedTextWriter.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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 System.Threading.Tasks; +using Microsoft.Framework.Runtime; + +namespace Microsoft.AspNet.Mvc.Razor +{ + /// + /// Specifies the contracts for a that buffers its content. + /// + [AssemblyNeutral] + public interface IBufferedTextWriter + { + /// + /// Gets a flag that determines if content is currently being buffered. + /// + bool IsBuffering { get; } + + /// + /// Copies the buffered content to the . + /// + /// The writer to copy the contents to./param> + void CopyTo(TextWriter writer); + + /// + /// Asynchronously copies the buffered content to the . + /// + /// The writer to copy the contents to./param> + /// A representing the copy operation. + Task CopyToAsync(TextWriter writer); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/Services/IPageExecutionContext.cs b/src/Microsoft.AspNet.Mvc.Razor/Services/IPageExecutionContext.cs new file mode 100644 index 0000000000..51990d628f --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/Services/IPageExecutionContext.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Framework.Runtime; + +namespace Microsoft.AspNet.PageExecutionInstrumentation +{ + /// + /// Specifies the contracts for a execution context that instruments web page execution. + /// + [AssemblyNeutral] + public interface IPageExecutionContext + { + /// + /// Invoked at the start of a write operation. + /// + /// The absolute character position of the expression or text in the Razor file. + /// The character length of the expression or text in the Razor file. + /// A flag that indicates if the operation is for a literal text and not for a + /// language expression. + void BeginContext(int position, int length, bool isLiteral); + + /// + /// Invoked at the end of a write operation. + /// + void EndContext(); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/Services/IPageExecutionListenerFeature.cs b/src/Microsoft.AspNet.Mvc.Razor/Services/IPageExecutionListenerFeature.cs new file mode 100644 index 0000000000..70e80076b6 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/Services/IPageExecutionListenerFeature.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Framework.Runtime; + +namespace Microsoft.AspNet.PageExecutionInstrumentation +{ + /// + /// Specifies the contracts for a HTTP feature that provides the context to instrument a web page. + /// + [AssemblyNeutral] + public interface IPageExecutionListenerFeature + { + /// + /// Decorates the used by web page instances to + /// write the result to. + /// + /// The output for the web page. + /// A that wraps . + /// + TextWriter DecorateWriter(TextWriter writer); + + /// + /// Creates a for the specified . + /// + /// The path of the . + /// The obtained from . + /// + /// + IPageExecutionContext GetContext(string sourceFilePath, TextWriter writer); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/InjectChunkVisitorTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/InjectChunkVisitorTest.cs index afd458d64f..65b9aaf818 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/InjectChunkVisitorTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/InjectChunkVisitorTest.cs @@ -2,14 +2,10 @@ // 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.AspNet.Razor; using Microsoft.AspNet.Razor.Generator; using Microsoft.AspNet.Razor.Generator.Compiler; using Microsoft.AspNet.Razor.Generator.Compiler.CSharp; using Microsoft.AspNet.Razor.Parser.SyntaxTree; -using Microsoft.AspNet.Razor.Text; using Xunit; namespace Microsoft.AspNet.Mvc.Razor diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs index 75a14a7bd1..accaa77f29 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs @@ -5,8 +5,8 @@ using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; -using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.AspNet.PageExecutionInstrumentation; using Microsoft.AspNet.PipelineCore; using Microsoft.AspNet.Testing; using Moq; @@ -351,6 +351,68 @@ Layout end Assert.DoesNotThrow(() => page.SectionWriters["test-section"].WriteTo(TextWriter.Null)); } + [Fact] + public async Task WriteAttribute_CallsBeginAndEndContext_OnPageExecutionListenerContext() + { + // Arrange + var page = CreatePage(p => + { + p.WriteAttribute("href", + new PositionTagged("prefix", 0), + new PositionTagged("suffix", 34), + new AttributeValue(new PositionTagged("prefix", 0), + new PositionTagged("attr1-value", 8), + literal: true), + new AttributeValue(new PositionTagged("prefix2", 22), + new PositionTagged("attr2", 29), + literal: false)); + }); + var context = new Mock(MockBehavior.Strict); + var sequence = new MockSequence(); + context.InSequence(sequence).Setup(f => f.BeginContext(0, 6, true)).Verifiable(); + context.InSequence(sequence).Setup(f => f.EndContext()).Verifiable(); + context.InSequence(sequence).Setup(f => f.BeginContext(8, 14, true)).Verifiable(); + context.InSequence(sequence).Setup(f => f.EndContext()).Verifiable(); + context.InSequence(sequence).Setup(f => f.BeginContext(22, 7, true)).Verifiable(); + context.InSequence(sequence).Setup(f => f.EndContext()).Verifiable(); + context.InSequence(sequence).Setup(f => f.BeginContext(29, 5, false)).Verifiable(); + context.InSequence(sequence).Setup(f => f.EndContext()).Verifiable(); + context.InSequence(sequence).Setup(f => f.BeginContext(34, 6, true)).Verifiable(); + context.InSequence(sequence).Setup(f => f.EndContext()).Verifiable(); + page.PageExecutionContext = context.Object; + + // Act + await page.ExecuteAsync(); + + // Assert + context.Verify(); + } + + [Fact] + public async Task WriteAttribute_CallsBeginAndEndContext_OnPrefixAndSuffixValues() + { + // Arrange + var page = CreatePage(p => + { + p.WriteAttribute("href", + new PositionTagged("prefix", 0), + new PositionTagged("tail", 7)); + }); + var context = new Mock(MockBehavior.Strict); + var sequence = new MockSequence(); + context.InSequence(sequence).Setup(f => f.BeginContext(0, 6, true)).Verifiable(); + context.InSequence(sequence).Setup(f => f.EndContext()).Verifiable(); + context.InSequence(sequence).Setup(f => f.BeginContext(7, 4, true)).Verifiable(); + context.InSequence(sequence).Setup(f => f.EndContext()).Verifiable(); + page.PageExecutionContext = context.Object; + + // Act + await page.ExecuteAsync(); + + // Assert + context.Verify(); + } + private static TestableRazorPage CreatePage(Action executeAction, ViewContext context = null) {