From 12477c9f52be0b6f7cdf9eb71953b6dbbbd3b5da Mon Sep 17 00:00:00 2001 From: Pranav K Date: Thu, 2 Oct 2014 17:49:54 -0700 Subject: [PATCH] Changes to make EnableInstrumentation conditionally enabled --- Mvc.sln | 15 +- .../Compilation/CompilerCache.cs | 31 ++- .../Compilation/CompilerCacheEntry.cs | 59 ++++- .../IRazorPageFactory.cs | 3 +- src/Microsoft.AspNet.Mvc.Razor/IRazorView.cs | 5 +- .../IViewStartProvider.cs | 3 +- .../Razor/IRazorCompilationService.cs | 13 +- .../Razor/RazorCompilationService.cs | 16 +- src/Microsoft.AspNet.Mvc.Razor/RazorView.cs | 80 ++++--- .../RazorViewEngine.cs | 26 ++- .../ViewStartProvider.cs | 9 +- .../VirtualPathRazorPageFactory.cs | 4 +- src/Microsoft.AspNet.Mvc/MvcServices.cs | 8 +- .../RazorInstrumentationTests.cs | 154 +++++++++++++ .../project.json | 1 + .../Compilation/CompilerCacheTest.cs | 46 +++- .../RazorCompilationServiceTest.cs | 45 +++- .../RazorViewEngineTest.cs | 18 +- .../RazorViewTest.cs | 204 +++++++++++++++--- .../HomeController.cs | 21 ++ .../RazorInstrumentationWebsite.kproj | 21 ++ .../RazorInstrumentationWebsite/Startup.cs | 42 ++++ .../TestPageExecutionContext.cs | 24 +++ .../TestPageExecutionListenerFeature.cs | 28 +++ .../Views/Home/FullPath.cshtml | 3 + .../Views/Home/ViewDiscoveryPath.cshtml | 3 + .../Views/_Layout.cshtml | 4 + .../Views/_ViewStart.cshtml | 5 + .../RazorInstrumentationWebsite/project.json | 11 + 29 files changed, 791 insertions(+), 111 deletions(-) create mode 100644 test/Microsoft.AspNet.Mvc.FunctionalTests/RazorInstrumentationTests.cs create mode 100644 test/WebSites/RazorInstrumentationWebsite/HomeController.cs create mode 100644 test/WebSites/RazorInstrumentationWebsite/RazorInstrumentationWebsite.kproj create mode 100644 test/WebSites/RazorInstrumentationWebsite/Startup.cs create mode 100644 test/WebSites/RazorInstrumentationWebsite/TestPageExecutionContext.cs create mode 100644 test/WebSites/RazorInstrumentationWebsite/TestPageExecutionListenerFeature.cs create mode 100644 test/WebSites/RazorInstrumentationWebsite/Views/Home/FullPath.cshtml create mode 100644 test/WebSites/RazorInstrumentationWebsite/Views/Home/ViewDiscoveryPath.cshtml create mode 100644 test/WebSites/RazorInstrumentationWebsite/Views/_Layout.cshtml create mode 100644 test/WebSites/RazorInstrumentationWebsite/Views/_ViewStart.cshtml create mode 100644 test/WebSites/RazorInstrumentationWebsite/project.json diff --git a/Mvc.sln b/Mvc.sln index 6907bee3b5..13b6839de7 100644 --- a/Mvc.sln +++ b/Mvc.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.22013.1 +VisualStudioVersion = 14.0.22130.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}" EndProject @@ -86,6 +86,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ReflectedModelWebSite", "te EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "FilesWebSite", "test\WebSites\FilesWebSite\FilesWebSite.kproj", "{0EF9860B-10D7-452F-B0F4-A405B88BEBB3}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "RazorInstrumentationWebSite", "test\WebSites\RazorInstrumentationWebSite\RazorInstrumentationWebSite.kproj", "{2B2B9876-903C-4065-8D62-2EE832BBA106}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -446,6 +448,16 @@ Global {C2EF54F8-8886-4260-A322-44F76245F95D}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {C2EF54F8-8886-4260-A322-44F76245F95D}.Release|Mixed Platforms.Build.0 = Release|Any CPU {C2EF54F8-8886-4260-A322-44F76245F95D}.Release|x86.ActiveCfg = Release|Any CPU + {2B2B9876-903C-4065-8D62-2EE832BBA106}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2B2B9876-903C-4065-8D62-2EE832BBA106}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2B2B9876-903C-4065-8D62-2EE832BBA106}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {2B2B9876-903C-4065-8D62-2EE832BBA106}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {2B2B9876-903C-4065-8D62-2EE832BBA106}.Debug|x86.ActiveCfg = Debug|Any CPU + {2B2B9876-903C-4065-8D62-2EE832BBA106}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2B2B9876-903C-4065-8D62-2EE832BBA106}.Release|Any CPU.Build.0 = Release|Any CPU + {2B2B9876-903C-4065-8D62-2EE832BBA106}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {2B2B9876-903C-4065-8D62-2EE832BBA106}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {2B2B9876-903C-4065-8D62-2EE832BBA106}.Release|x86.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -487,5 +499,6 @@ Global {0EF9860B-10D7-452F-B0F4-A405B88BEBB3} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {61061528-071E-424E-965A-07BCC2F02672} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {C2EF54F8-8886-4260-A322-44F76245F95D} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} + {2B2B9876-903C-4065-8D62-2EE832BBA106} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} EndGlobalSection EndGlobal diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs index bd19a929b0..2a99db122a 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs @@ -64,25 +64,30 @@ namespace Microsoft.AspNet.Mvc.Razor return false; } - public CompilationResult GetOrAdd(RelativeFileInfo fileInfo, Func compile) + public CompilationResult GetOrAdd([NotNull] RelativeFileInfo fileInfo, + bool enableInstrumentation, + [NotNull] Func compile) { CompilerCacheEntry cacheEntry; if (!_cache.TryGetValue(fileInfo.RelativePath, out cacheEntry)) { - return OnCacheMiss(fileInfo, compile); + return OnCacheMiss(fileInfo, enableInstrumentation, compile); } else { - if (cacheEntry.Length != fileInfo.FileInfo.Length) + if ((cacheEntry.Length != fileInfo.FileInfo.Length) || + (enableInstrumentation && !cacheEntry.IsInstrumented)) { - // it's not a match, recompile - return OnCacheMiss(fileInfo, compile); + // Recompile if + // (a) If the file lengths differ + // (b) If the compiled type is not instrumented but we require it to be instrumented. + return OnCacheMiss(fileInfo, enableInstrumentation, compile); } if (cacheEntry.LastModified == fileInfo.FileInfo.LastModified) { // Match, not update needed - return CompilationResult.Successful(cacheEntry.ViewType); + return CompilationResult.Successful(cacheEntry.CompiledType); } var hash = RazorFileHash.GetHash(fileInfo.FileInfo); @@ -92,20 +97,24 @@ namespace Microsoft.AspNet.Mvc.Razor string.Equals(cacheEntry.Hash, hash, StringComparison.Ordinal)) { // Cache hit, but we need to update the entry - return OnCacheMiss(fileInfo, () => CompilationResult.Successful(cacheEntry.ViewType)); + return OnCacheMiss(fileInfo, + enableInstrumentation, + () => CompilationResult.Successful(cacheEntry.CompiledType)); } // it's not a match, recompile - return OnCacheMiss(fileInfo, compile); + return OnCacheMiss(fileInfo, enableInstrumentation, compile); } } - private CompilationResult OnCacheMiss(RelativeFileInfo file, Func compile) + private CompilationResult OnCacheMiss(RelativeFileInfo file, + bool isInstrumented, + Func compile) { var result = compile(); - var cacheEntry = new CompilerCacheEntry(file, result.CompiledType); - _cache.AddOrUpdate(file.RelativePath, cacheEntry, (a, b) => cacheEntry); + var cacheEntry = new CompilerCacheEntry(file, result.CompiledType, isInstrumented); + _cache[file.RelativePath] = cacheEntry; return result; } diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCacheEntry.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCacheEntry.cs index 107ddf757b..0a395e73ee 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCacheEntry.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCacheEntry.cs @@ -5,35 +5,74 @@ using System; namespace Microsoft.AspNet.Mvc.Razor { + /// + /// An entry in that contain metadata about precompiled and dynamically compiled file. + /// public class CompilerCacheEntry { - public CompilerCacheEntry([NotNull] RazorFileInfo info, [NotNull] Type viewType) + /// + /// Initializes a new instance of for a file that was precompiled. + /// + /// Metadata about the precompiled file. + /// The compiled . + public CompilerCacheEntry([NotNull] RazorFileInfo info, [NotNull] Type compiledType) { - ViewType = viewType; + CompiledType = compiledType; RelativePath = info.RelativePath; Length = info.Length; LastModified = info.LastModified; Hash = info.Hash; } - public CompilerCacheEntry([NotNull] RelativeFileInfo info, [NotNull] Type viewType) + /// + /// Initializes a new instance of for a file that was dynamically compiled. + /// + /// Metadata about the file that was compiled. + /// The compiled . + /// Flag that indicates that the file was generated with instrumentation + /// enabled. + public CompilerCacheEntry([NotNull] RelativeFileInfo info, [NotNull] Type compiledType, bool isInstrumented) { - ViewType = viewType; + CompiledType = compiledType; RelativePath = info.RelativePath; Length = info.FileInfo.Length; LastModified = info.FileInfo.LastModified; + IsInstrumented = isInstrumented; } - public Type ViewType { get; set; } - public string RelativePath { get; set; } - public long Length { get; set; } - public DateTime LastModified { get; set; } + /// + /// Gets the produced as a result of compilation. + /// + public Type CompiledType { get; private set; } /// - /// The file hash, should only be available for pre compiled files. + /// Gets the path of the compiled file relative to the root of the application. /// - public string Hash { get; set; } + public string RelativePath { get; private set; } + /// + /// Gets the size of file (in bytes) on disk. + /// + public long Length { get; private set; } + + /// + /// Gets the last modified for the file that was compiled at the time of compilation. + /// + public DateTime LastModified { get; private set; } + + /// + /// Gets the file hash, should only be available for pre compiled files. + /// + public string Hash { get; private set; } + + /// + /// Gets a flag that indicates if the file is precompiled. + /// public bool IsPreCompiled { get { return Hash != null; } } + + /// + /// Gets a flag that indiciates if the page execution in is instrumeted. + /// + public bool IsInstrumented { get; private set; } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/IRazorPageFactory.cs b/src/Microsoft.AspNet.Mvc.Razor/IRazorPageFactory.cs index daaf28222e..b84b180625 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/IRazorPageFactory.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/IRazorPageFactory.cs @@ -12,7 +12,8 @@ namespace Microsoft.AspNet.Mvc.Razor /// Creates a for the specified path. /// /// The path to locate the page. + /// Indicates that execution of the page should be instrumented. /// The IRazorPage instance if it exists, null otherwise. - IRazorPage CreateInstance(string relativePath); + IRazorPage CreateInstance(string relativePath, bool enableInstrumentation); } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/IRazorView.cs b/src/Microsoft.AspNet.Mvc.Razor/IRazorView.cs index 7f8f411a7e..a5e261d9c9 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/IRazorView.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/IRazorView.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.AspNet.PageExecutionInstrumentation; namespace Microsoft.AspNet.Mvc.Razor { @@ -17,6 +18,8 @@ namespace Microsoft.AspNet.Mvc.Razor /// /// The instance to execute. /// Determines if the view is to be executed as a partial. - void Contextualize(IRazorPage razorPage, bool isPartial); + void Contextualize(IRazorPage razorPage, + bool isPartial, + IPageExecutionListenerFeature pageExecutionListenerFeature); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/IViewStartProvider.cs b/src/Microsoft.AspNet.Mvc.Razor/IViewStartProvider.cs index 47a445d04b..2076af472f 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/IViewStartProvider.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/IViewStartProvider.cs @@ -15,7 +15,8 @@ namespace Microsoft.AspNet.Mvc.Razor /// that are applicable to the specified view. /// /// The path of the page to locate ViewStart files for. + /// Indicates that execution of the page should be instrumented. /// A sequence of that represent ViewStart. - IEnumerable GetViewStartPages(string path); + IEnumerable GetViewStartPages(string path, bool enableInstrumentation); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/Razor/IRazorCompilationService.cs b/src/Microsoft.AspNet.Mvc.Razor/Razor/IRazorCompilationService.cs index 6cc16d396e..d9a59c2443 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Razor/IRazorCompilationService.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Razor/IRazorCompilationService.cs @@ -3,8 +3,19 @@ namespace Microsoft.AspNet.Mvc.Razor { + /// + /// Specifies the contracts for a service that compiles Razor files. + /// public interface IRazorCompilationService { - CompilationResult Compile(RelativeFileInfo fileInfo); + /// + /// Compiles the razor file located at . + /// + /// A instance that represents the file to compile. + /// + /// Indicates that the page should be instrumented. + /// A that represents the results of parsing and compiling the file. + /// + CompilationResult Compile(RelativeFileInfo fileInfo, bool isInstrumented); } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/Razor/RazorCompilationService.cs b/src/Microsoft.AspNet.Mvc.Razor/Razor/RazorCompilationService.cs index af77886ad6..e8b463f205 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Razor/RazorCompilationService.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Razor/RazorCompilationService.cs @@ -6,9 +6,14 @@ using Microsoft.AspNet.Razor; namespace Microsoft.AspNet.Mvc.Razor { + /// + /// Default implementation of . + /// + /// + /// This class must be registered as a singleton service for the caching to work. + /// public class RazorCompilationService : IRazorCompilationService { - // This class must be registered as a singleton service for the caching to work. private readonly CompilerCache _cache; private readonly ICompilationService _baseCompilationService; private readonly IMvcRazorHost _razorHost; @@ -22,13 +27,16 @@ namespace Microsoft.AspNet.Mvc.Razor _cache = new CompilerCache(_controllerAssemblyProvider.CandidateAssemblies); } - public CompilationResult Compile([NotNull] RelativeFileInfo file) + /// + public CompilationResult Compile([NotNull] RelativeFileInfo file, bool isInstrumented) { - return _cache.GetOrAdd(file, () => CompileCore(file)); + return _cache.GetOrAdd(file, isInstrumented, () => CompileCore(file, isInstrumented)); } - internal CompilationResult CompileCore(RelativeFileInfo file) + internal CompilationResult CompileCore(RelativeFileInfo file, bool isInstrumented) { + _razorHost.EnableInstrumentation = isInstrumented; + GeneratorResults results; using (var inputStream = file.FileInfo.CreateReadStream()) { diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs index b9d076de0f..a27e9f2a18 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs @@ -2,7 +2,9 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.IO; using System.Threading.Tasks; +using Microsoft.AspNet.PageExecutionInstrumentation; namespace Microsoft.AspNet.Mvc.Razor { @@ -15,6 +17,7 @@ namespace Microsoft.AspNet.Mvc.Razor private readonly IRazorPageFactory _pageFactory; private readonly IRazorPageActivator _pageActivator; private readonly IViewStartProvider _viewStartProvider; + private IPageExecutionListenerFeature _pageExecutionFeature; private IRazorPage _razorPage; private bool _isPartial; @@ -34,11 +37,19 @@ namespace Microsoft.AspNet.Mvc.Razor _viewStartProvider = viewStartProvider; } + private bool EnableInstrumentation + { + get { return _pageExecutionFeature != null; } + } + /// - public virtual void Contextualize(IRazorPage razorPage, bool isPartial) + public virtual void Contextualize([NotNull] IRazorPage razorPage, + bool isPartial, + IPageExecutionListenerFeature pageExecutionListener) { _razorPage = razorPage; _isPartial = isPartial; + _pageExecutionFeature = pageExecutionListener; } /// @@ -61,45 +72,66 @@ namespace Microsoft.AspNet.Mvc.Razor } } - private async Task RenderPageAsync(IRazorPage page, - ViewContext context, - bool executeViewStart) + private async Task RenderPageAsync(IRazorPage page, + ViewContext context, + bool executeViewStart) { - using (var bufferedWriter = new RazorTextWriter(context.Writer, context.Writer.Encoding)) + var razorTextWriter = new RazorTextWriter(context.Writer, context.Writer.Encoding); + TextWriter writer = razorTextWriter; + IBufferedTextWriter bufferedWriter = razorTextWriter; + + if (EnableInstrumentation) { - // The writer for the body is passed through the ViewContext, allowing things like HtmlHelpers - // and ViewComponents to reference it. - var oldWriter = context.Writer; - context.Writer = bufferedWriter; - - try + writer = _pageExecutionFeature.DecorateWriter(razorTextWriter); + bufferedWriter = writer as IBufferedTextWriter; + if (bufferedWriter == null) { - if (executeViewStart) - { - // Execute view starts using the same context + writer as the page to render. - await RenderViewStartAsync(context); - } + var message = Resources.FormatInstrumentation_WriterMustBeBufferedTextWriter( + nameof(TextWriter), + _pageExecutionFeature.GetType().FullName, + typeof(IBufferedTextWriter).FullName); + throw new InvalidOperationException(message); + } + } - await RenderPageCoreAsync(page, context); - return bufferedWriter; - } - finally + // The writer for the body is passed through the ViewContext, allowing things like HtmlHelpers + // and ViewComponents to reference it. + var oldWriter = context.Writer; + context.Writer = writer; + + try + { + if (executeViewStart) { - context.Writer = oldWriter; + // Execute view starts using the same context + writer as the page to render. + await RenderViewStartAsync(context); } + + await RenderPageCoreAsync(page, context); + return bufferedWriter; + } + finally + { + context.Writer = oldWriter; + writer.Dispose(); } } private async Task RenderPageCoreAsync(IRazorPage page, ViewContext context) { page.ViewContext = context; + if (EnableInstrumentation) + { + page.PageExecutionContext = _pageExecutionFeature.GetContext(page.Path, context.Writer); + } + _pageActivator.Activate(page, context); await page.ExecuteAsync(); } private async Task RenderViewStartAsync(ViewContext context) { - var viewStarts = _viewStartProvider.GetViewStartPages(_razorPage.Path); + var viewStarts = _viewStartProvider.GetViewStartPages(_razorPage.Path, EnableInstrumentation); foreach (var viewStart in viewStarts) { @@ -111,7 +143,7 @@ namespace Microsoft.AspNet.Mvc.Razor } private async Task RenderLayoutAsync(ViewContext context, - RazorTextWriter bodyWriter) + IBufferedTextWriter bodyWriter) { // A layout page can specify another layout page. We'll need to continue // looking for layout pages until they're no longer specified. @@ -129,7 +161,7 @@ namespace Microsoft.AspNet.Mvc.Razor throw new InvalidOperationException(message); } - var layoutPage = _pageFactory.CreateInstance(previousPage.Layout); + var layoutPage = _pageFactory.CreateInstance(previousPage.Layout, EnableInstrumentation); if (layoutPage == null) { var message = Resources.FormatLayoutCannotBeLocated(previousPage.Layout); diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs index bdc123112a..b8d4034b28 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs @@ -3,9 +3,11 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using Microsoft.AspNet.Mvc.Razor.OptionDescriptors; using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.AspNet.PageExecutionInstrumentation; using Microsoft.Framework.DependencyInjection; namespace Microsoft.AspNet.Mvc.Razor @@ -35,6 +37,9 @@ namespace Microsoft.AspNet.Mvc.Razor private readonly IRazorPageFactory _pageFactory; private readonly IReadOnlyList _viewLocationExpanders; private readonly IViewLocationCache _viewLocationCache; + // The RazorViewEngine is Request scoped which allows us to cache these value for the lifetime of a Request. + private bool _isPageExecutionFeatureInitialized; + private IPageExecutionListenerFeature _pageExecutionListenerFeature; /// /// Initializes a new instance of the class. @@ -90,7 +95,7 @@ namespace Microsoft.AspNet.Mvc.Razor { if (viewName.EndsWith(ViewExtension, StringComparison.OrdinalIgnoreCase)) { - var page = _pageFactory.CreateInstance(viewName); + var page = _pageFactory.CreateInstance(viewName, IsInstrumentationEnabled(context)); if (page != null) { return CreateFoundResult(context, page, viewName, partial); @@ -132,7 +137,7 @@ namespace Microsoft.AspNet.Mvc.Razor var viewLocation = _viewLocationCache.Get(expanderContext); if (!string.IsNullOrEmpty(viewLocation)) { - var page = _pageFactory.CreateInstance(viewLocation); + var page = _pageFactory.CreateInstance(viewLocation, IsInstrumentationEnabled(context)); if (page != null) { @@ -158,7 +163,7 @@ namespace Microsoft.AspNet.Mvc.Razor viewName, controllerName, areaName); - var page = _pageFactory.CreateInstance(transformedPath); + var page = _pageFactory.CreateInstance(transformedPath, IsInstrumentationEnabled(context)); if (page != null) { // 3a. We found a page. Cache the set of values that produced it and return a found result. @@ -183,7 +188,9 @@ namespace Microsoft.AspNet.Mvc.Razor var services = actionContext.HttpContext.RequestServices; var view = services.GetService(); - view.Contextualize(page, partial); + Debug.Assert(_isPageExecutionFeatureInitialized, "IsInstrumentationEnabled must be called prior to this."); + + view.Contextualize(page, partial, _pageExecutionListenerFeature); return ViewEngineResult.Found(viewName, view); } @@ -191,5 +198,16 @@ namespace Microsoft.AspNet.Mvc.Razor { return name[0] == '~' || name[0] == '/'; } + + private bool IsInstrumentationEnabled(ActionContext context) + { + if (!_isPageExecutionFeatureInitialized) + { + _isPageExecutionFeatureInitialized = true; + _pageExecutionListenerFeature = context.HttpContext.GetFeature(); + } + + return _pageExecutionListenerFeature != null; + } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/ViewStartProvider.cs b/src/Microsoft.AspNet.Mvc.Razor/ViewStartProvider.cs index 1c56ea4909..a22e73d996 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/ViewStartProvider.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/ViewStartProvider.cs @@ -22,14 +22,13 @@ namespace Microsoft.AspNet.Mvc.Razor _pageFactory = pageFactory; } - /// - public IEnumerable GetViewStartPages([NotNull] string path) + public IEnumerable GetViewStartPages([NotNull] string path, bool enableInstrumentation) { var viewStartLocations = ViewStartUtility.GetViewStartLocations(_fileSystem, path); - var viewStarts = viewStartLocations.Select(_pageFactory.CreateInstance) - .Where(p => p != null) - .ToArray(); + var viewStarts = viewStartLocations.Select(p => _pageFactory.CreateInstance(p, enableInstrumentation)) + .Where(p => p != null) + .ToArray(); // GetViewStartLocations return ViewStarts inside-out that is the _ViewStart closest to the page // is the first: e.g. [ /Views/Home/_ViewStart, /Views/_ViewStart, /_ViewStart ] diff --git a/src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs b/src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs index 304a0b9509..2930669267 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs @@ -30,7 +30,7 @@ namespace Microsoft.AspNet.Mvc.Razor } /// - public IRazorPage CreateInstance([NotNull] string relativePath) + public IRazorPage CreateInstance([NotNull] string relativePath, bool enableInstrumentation) { var fileInfo = _fileInfoCache.GetFileInfo(relativePath); @@ -42,7 +42,7 @@ namespace Microsoft.AspNet.Mvc.Razor RelativePath = relativePath, }; - var result = _compilationService.Compile(relativeFileInfo); + var result = _compilationService.Compile(relativeFileInfo, enableInstrumentation); var page = (IRazorPage)_activator.CreateInstance(_serviceProvider, result.CompiledType); page.Path = relativePath; diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs index a236e8d32c..8e0f8fd211 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServices.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServices.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; -using Microsoft.AspNet.Mvc.Core; using Microsoft.AspNet.Mvc.Description; using Microsoft.AspNet.Mvc.Filters; using Microsoft.AspNet.Mvc.Internal; @@ -49,7 +48,10 @@ namespace Microsoft.AspNet.Mvc yield return describe.Singleton(); yield return describe.Singleton(); - yield return describe.Singleton(); + + // The provider is inexpensive to initialize and provides ViewEngines that may require request + // specific services. + yield return describe.Transient(); yield return describe.Scoped(); yield return describe.Singleton(); yield return describe.Transient(); @@ -112,7 +114,7 @@ namespace Microsoft.AspNet.Mvc yield return describe.Singleton(); - yield return describe.Transient, + yield return describe.Transient, DefaultApiDescriptionProvider>(); yield return diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/RazorInstrumentationTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/RazorInstrumentationTests.cs new file mode 100644 index 0000000000..bc0e0a88bf --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/RazorInstrumentationTests.cs @@ -0,0 +1,154 @@ +// 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; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.TestHost; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.DependencyInjection.Fallback; +using RazorInstrumentationWebSite; +using Xunit; + +namespace Microsoft.AspNet.Mvc.FunctionalTests +{ + public class RazorInstrumentationTests + { + private readonly IServiceProvider _services = TestHelper.CreateServices("RazorInstrumentationWebsite"); + private readonly Action _app = new Startup().Configure; + private readonly string _expected = string.Join(Environment.NewLine, + @"
", + @"2147483647", + "", + @"viewstart-content", + @"

", + @"page-content", + @"

", + @"
"); + private readonly IEnumerable> _expectedLineMappings = new[] + { + Tuple.Create(93, 2, true), + Tuple.Create(96, 16, false), + Tuple.Create(112, 2, true), + Tuple.Create(0, 2, true), + Tuple.Create(2, 8, true), + Tuple.Create(10, 16, false), + Tuple.Create(26, 1, true), + Tuple.Create(27, 21, true), + Tuple.Create(0, 7, true), + Tuple.Create(8, 12, false), + Tuple.Create(20, 2, true), + Tuple.Create(23, 12, false), + Tuple.Create(35, 8, true), + }; + + public static IEnumerable ActionNames + { + get + { + yield return new[] { "FullPath" }; + yield return new[] { "ViewDiscoveryPath" }; + } + } + + [Theory] + [MemberData(nameof(ActionNames))] + public async Task ViewsAreServedWithoutInstrumentationByDefault(string actionName) + { + // Arrange + var context = new TestPageExecutionContext(); + var services = GetServiceProvider(context); + var server = TestServer.Create(services, _app); + var client = server.CreateClient(); + + // Act + var body = await client.GetStringAsync("http://localhost/Home/" + actionName); + + // Assert + Assert.Equal(_expected, body.Trim()); + Assert.Empty(context.Values); + } + + [Theory] + [MemberData(nameof(ActionNames))] + public async Task ViewsAreInstrumentedWhenPageExecutionListenerFeatureIsEnabled(string actionName) + { + // Arrange + var context = new TestPageExecutionContext(); + var services = GetServiceProvider(context); + var server = TestServer.Create(services, _app); + var client = server.CreateClient(); + client.DefaultRequestHeaders.Add("ENABLE-RAZOR-INSTRUMENTATION", "true"); + + // Act + var body = await client.GetStringAsync("http://localhost/Home/" + actionName); + + // Assert + Assert.Equal(_expected, body.Trim()); + Assert.Equal(_expectedLineMappings, context.Values); + } + + [Theory] + [MemberData(nameof(ActionNames))] + public async Task ViewsCanSwitchFromRegularToInstrumented(string actionName) + { + // Arrange - 1 + var context = new TestPageExecutionContext(); + var services = GetServiceProvider(context); + var server = TestServer.Create(services, _app); + var client = server.CreateClient(); + + // Act - 1 + var body = await client.GetStringAsync("http://localhost/Home/" + actionName); + + // Assert - 1 + Assert.Equal(_expected, body.Trim()); + Assert.Empty(context.Values); + + // Arrange - 2 + client.DefaultRequestHeaders.Add("ENABLE-RAZOR-INSTRUMENTATION", "true"); + + // Act - 2 + body = await client.GetStringAsync("http://localhost/Home/" + actionName); + + // Assert - 2 + Assert.Equal(_expected, body.Trim()); + Assert.Equal(_expectedLineMappings, context.Values); + } + + [Fact] + public async Task SwitchingFromNonInstrumentedToInstrumentedWorksForLayoutAndViewStarts() + { + // Arrange - 1 + var context = new TestPageExecutionContext(); + var services = GetServiceProvider(context); + var server = TestServer.Create(services, _app); + var client = server.CreateClient(); + + // Act - 1 + var body = await client.GetStringAsync("http://localhost/Home/FullPath"); + + // Assert - 1 + Assert.Equal(_expected, body.Trim()); + Assert.Empty(context.Values); + + // Arrange - 2 + client.DefaultRequestHeaders.Add("ENABLE-RAZOR-INSTRUMENTATION", "true"); + + // Act - 2 + body = await client.GetStringAsync("http://localhost/Home/ViewDiscoveryPath"); + + // Assert - 2 + Assert.Equal(_expected, body.Trim()); + Assert.Equal(_expectedLineMappings, context.Values); + } + + private IServiceProvider GetServiceProvider(TestPageExecutionContext pageExecutionContext) + { + var services = new ServiceCollection(); + services.AddInstance(pageExecutionContext); + return services.BuildServiceProvider(_services); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json index 526e766a55..7d1ad1ecef 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json @@ -28,6 +28,7 @@ "ReflectedModelWebSite": "", "RoutingWebSite": "", "RazorWebSite": "", + "RazorInstrumentationWebsite": "", "ValueProvidersSite": "", "XmlSerializerWebSite": "", "UrlHelperWebSite": "", diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs index 3230f626fc..d11654d5df 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs @@ -34,7 +34,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test }; // Act - var actual = cache.GetOrAdd(runtimeFileInfo, () => expected); + var actual = cache.GetOrAdd(runtimeFileInfo, false, () => expected); // Assert Assert.Same(expected, actual); @@ -140,7 +140,8 @@ namespace Microsoft.AspNet.Mvc.Razor.Test // Act var actual = cache.GetOrAdd(runtimeFileInfo, - () => CompilationResult.Successful(resultViewType)); + enableInstrumentation: false, + compile: () => CompilationResult.Successful(resultViewType)); // Assert if (swapsPreCompile) @@ -174,9 +175,9 @@ namespace Microsoft.AspNet.Mvc.Razor.Test }; // Act - cache.GetOrAdd(runtimeFileInfo, () => uncachedResult); - var actual1 = cache.GetOrAdd(runtimeFileInfo, () => uncachedResult); - var actual2 = cache.GetOrAdd(runtimeFileInfo, () => uncachedResult); + cache.GetOrAdd(runtimeFileInfo, false, () => uncachedResult); + var actual1 = cache.GetOrAdd(runtimeFileInfo, false, () => uncachedResult); + var actual2 = cache.GetOrAdd(runtimeFileInfo, false, () => uncachedResult); // Assert Assert.NotSame(uncachedResult, actual1); @@ -189,5 +190,40 @@ namespace Microsoft.AspNet.Mvc.Razor.Test Assert.Null(actual2.CompiledContent); Assert.Same(type, actual2.CompiledType); } + + [Fact] + public void GetOrAdd_IgnoresCache_IfCachedItemIsNotInstrumentedAndEnableInstrumentationIsTrue() + { + // Arrange + var lastModified = DateTime.UtcNow; + var cache = new CompilerCache(); + var fileInfo = new Mock(); + fileInfo.SetupGet(f => f.PhysicalPath) + .Returns("test"); + fileInfo.SetupGet(f => f.LastModified) + .Returns(lastModified); + var type = GetType(); + var uncachedResult1 = UncachedCompilationResult.Successful(type, "hello world"); + var uncachedResult2 = UncachedCompilationResult.Successful(typeof(object), "hello world"); + var uncachedResult3 = UncachedCompilationResult.Successful(typeof(Guid), "hello world"); + + var runtimeFileInfo = new RelativeFileInfo() + { + FileInfo = fileInfo.Object, + RelativePath = "test", + }; + + // Act + cache.GetOrAdd(runtimeFileInfo, false, () => uncachedResult1); + var actual1 = cache.GetOrAdd(runtimeFileInfo, true, () => uncachedResult2); + var actual2 = cache.GetOrAdd(runtimeFileInfo, false, () => uncachedResult3); + + // Assert + Assert.Same(uncachedResult2, actual1); + Assert.Same(typeof(object), actual1.CompiledType); + + Assert.NotSame(actual2, uncachedResult3); + Assert.Same(typeof(object), actual2.CompiledType); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorCompilationServiceTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorCompilationServiceTest.cs index 93c2157e53..d1fb1d71f5 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorCompilationServiceTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorCompilationServiceTest.cs @@ -23,7 +23,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test // Arrange var host = new Mock(); host.Setup(h => h.GenerateCode(@"views\index\home.cshtml", It.IsAny())) - .Returns(new GeneratorResults(new Block(new BlockBuilder { Type = BlockType.Comment }), new RazorError[0], new CodeBuilderResult("", new LineMapping[0]))) + .Returns(GetGeneratorResult()) .Verifiable(); var ap = new Mock(); @@ -46,10 +46,51 @@ namespace Microsoft.AspNet.Mvc.Razor.Test }; // Act - razorService.CompileCore(relativeFileInfo); + razorService.CompileCore(relativeFileInfo, isInstrumented: false); // Assert host.Verify(); } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void CompileCoreSetsEnableInstrumentationOnHost(bool enableInstrumentation) + { + // Arrange + var host = new Mock(); + host.SetupAllProperties(); + host.Setup(h => h.GenerateCode(It.IsAny(), It.IsAny())) + .Returns(GetGeneratorResult()); + var assemblyProvider = new Mock(); + assemblyProvider.SetupGet(e => e.CandidateAssemblies) + .Returns(Enumerable.Empty()); + + var compiler = new Mock(); + compiler.Setup(c => c.Compile(It.IsAny(), It.IsAny())) + .Returns(CompilationResult.Successful(GetType())); + + var razorService = new RazorCompilationService(compiler.Object, assemblyProvider.Object, host.Object); + var relativeFileInfo = new RelativeFileInfo() + { + FileInfo = Mock.Of(), + RelativePath = @"views\index\home.cshtml", + }; + + // Act + razorService.CompileCore(relativeFileInfo, isInstrumented: enableInstrumentation); + + // Assert + Assert.Equal(enableInstrumentation, host.Object.EnableInstrumentation); + } + + private static GeneratorResults GetGeneratorResult() + { + return new GeneratorResults( + new Block( + new BlockBuilder { Type = BlockType.Comment }), + new RazorError[0], + new CodeBuilderResult("", new LineMapping[0])); + } } } diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs index 9dea394ec9..e927d08de2 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs @@ -183,7 +183,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test // Arrange var pageFactory = new Mock(); var page = Mock.Of(); - pageFactory.Setup(p => p.CreateInstance(It.IsAny())) + pageFactory.Setup(p => p.CreateInstance(It.IsAny(), false)) .Returns(Mock.Of()); var viewEngine = CreateViewEngine(pageFactory.Object); var context = GetActionContext(_controllerTestContext); @@ -203,7 +203,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test // Arrange var pageFactory = new Mock(); var page = Mock.Of(); - pageFactory.Setup(p => p.CreateInstance("fake-path1/bar/test-view.rzr")) + pageFactory.Setup(p => p.CreateInstance("fake-path1/bar/test-view.rzr", false)) .Returns(Mock.Of()) .Verifiable(); var viewEngine = new OverloadedLocationViewEngine(pageFactory.Object, @@ -224,7 +224,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test // Arrange var pageFactory = new Mock(); var page = Mock.Of(); - pageFactory.Setup(p => p.CreateInstance("fake-area-path/foo/bar/test-view2.rzr")) + pageFactory.Setup(p => p.CreateInstance("fake-area-path/foo/bar/test-view2.rzr", false)) .Returns(Mock.Of()) .Verifiable(); var viewEngine = new OverloadedLocationViewEngine(pageFactory.Object, @@ -273,7 +273,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test { // Arrange var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance("test-string/bar.cshtml")) + pageFactory.Setup(p => p.CreateInstance("test-string/bar.cshtml", false)) .Returns(Mock.Of()) .Verifiable(); var expander1Result = new[] { "some-seed" }; @@ -325,9 +325,9 @@ namespace Microsoft.AspNet.Mvc.Razor.Test { // Arrange var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance("/Views/bar/baz.cshtml")) + pageFactory.Setup(p => p.CreateInstance("/Views/bar/baz.cshtml", false)) .Verifiable(); - pageFactory.Setup(p => p.CreateInstance("/Views/Shared/baz.cshtml")) + pageFactory.Setup(p => p.CreateInstance("/Views/Shared/baz.cshtml", false)) .Returns(Mock.Of()) .Verifiable(); var cache = GetViewLocationCache(); @@ -353,7 +353,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test { // Arrange var pageFactory = new Mock(MockBehavior.Strict); - pageFactory.Setup(p => p.CreateInstance("some-view-location")) + pageFactory.Setup(p => p.CreateInstance("some-view-location", false)) .Returns(Mock.Of()) .Verifiable(); var expander = new Mock(MockBehavior.Strict); @@ -384,10 +384,10 @@ namespace Microsoft.AspNet.Mvc.Razor.Test { // Arrange var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance("expired-location")) + pageFactory.Setup(p => p.CreateInstance("expired-location", false)) .Returns((IRazorPage)null) .Verifiable(); - pageFactory.Setup(p => p.CreateInstance("some-view-location")) + pageFactory.Setup(p => p.CreateInstance("some-view-location", false)) .Returns(Mock.Of()) .Verifiable(); var cacheMock = new Mock(); diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs index e3a50390b6..009273a94c 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs @@ -3,8 +3,10 @@ using System; using System.IO; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.PageExecutionInstrumentation; using Microsoft.AspNet.PipelineCore; using Moq; using Xunit; @@ -45,7 +47,7 @@ namespace Microsoft.AspNet.Mvc.Razor var view = new RazorView(Mock.Of(), Mock.Of(), CreateViewStartProvider()); - view.Contextualize(page, isPartial: true); + view.Contextualize(page, isPartial: true, pageExecutionListener: null); var viewContext = CreateViewContext(view); var expected = viewContext.Writer; @@ -72,7 +74,7 @@ namespace Microsoft.AspNet.Mvc.Razor var view = new RazorView(Mock.Of(), activator.Object, CreateViewStartProvider()); - view.Contextualize(page, isPartial: true); + view.Contextualize(page, isPartial: true, pageExecutionListener: null); var viewContext = CreateViewContext(view); var expectedWriter = viewContext.Writer; activator.Setup(a => a.Activate(page, It.IsAny())) @@ -102,7 +104,7 @@ namespace Microsoft.AspNet.Mvc.Razor var view = new RazorView(Mock.Of(), activator.Object, CreateViewStartProvider()); - view.Contextualize(page, isPartial: true); + view.Contextualize(page, isPartial: true, pageExecutionListener: null); var viewContext = CreateViewContext(view); // Act @@ -124,15 +126,16 @@ namespace Microsoft.AspNet.Mvc.Razor var view = new RazorView(pageFactory.Object, Mock.Of(), viewStartProvider); - view.Contextualize(page, isPartial: true); + view.Contextualize(page, isPartial: true, pageExecutionListener: null); var viewContext = CreateViewContext(view); // Act await view.RenderAsync(viewContext); // Assert - pageFactory.Verify(v => v.CreateInstance(It.IsAny()), Times.Never()); - Mock.Get(viewStartProvider).Verify(v => v.GetViewStartPages(It.IsAny()), Times.Never()); + pageFactory.Verify(v => v.CreateInstance(It.IsAny(), It.IsAny()), Times.Never()); + Mock.Get(viewStartProvider) + .Verify(v => v.GetViewStartPages(It.IsAny(), It.IsAny()), Times.Never()); } [Fact] @@ -147,7 +150,7 @@ namespace Microsoft.AspNet.Mvc.Razor var view = new RazorView(Mock.Of(), Mock.Of(), CreateViewStartProvider()); - view.Contextualize(page, isPartial: false); + view.Contextualize(page, isPartial: false, pageExecutionListener: null); var viewContext = CreateViewContext(view); var original = viewContext.Writer; @@ -170,7 +173,7 @@ namespace Microsoft.AspNet.Mvc.Razor var view = new RazorView(Mock.Of(), Mock.Of(), CreateViewStartProvider()); - view.Contextualize(page, isPartial: false); + view.Contextualize(page, isPartial: false, pageExecutionListener: null); var viewContext = CreateViewContext(view); var original = viewContext.Writer; @@ -195,7 +198,7 @@ namespace Microsoft.AspNet.Mvc.Razor var view = new RazorView(Mock.Of(), activator.Object, CreateViewStartProvider()); - view.Contextualize(page, isPartial: false); + view.Contextualize(page, isPartial: false, pageExecutionListener: null); var viewContext = CreateViewContext(view); @@ -237,7 +240,7 @@ namespace Microsoft.AspNet.Mvc.Razor var view = new RazorView(Mock.Of(), activator.Object, CreateViewStartProvider(viewStart1, viewStart2)); - view.Contextualize(page, isPartial: false); + view.Contextualize(page, isPartial: false, pageExecutionListener: null); var viewContext = CreateViewContext(view); // Act @@ -285,13 +288,13 @@ foot-content"; activator.Setup(a => a.Activate(layout, It.IsAny())) .Verifiable(); var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance(LayoutPath)) + pageFactory.Setup(p => p.CreateInstance(LayoutPath, false)) .Returns(layout); var view = new RazorView(pageFactory.Object, activator.Object, CreateViewStartProvider()); - view.Contextualize(page, isPartial: false); + view.Contextualize(page, isPartial: false, pageExecutionListener: null); var viewContext = CreateViewContext(view); // Act @@ -318,13 +321,13 @@ foot-content"; v.RenderBodyPublic(); }); var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance(LayoutPath)) + pageFactory.Setup(p => p.CreateInstance(LayoutPath, false)) .Returns(layout); var view = new RazorView(pageFactory.Object, Mock.Of(), CreateViewStartProvider()); - view.Contextualize(page, isPartial: false); + view.Contextualize(page, isPartial: false, pageExecutionListener: null); var viewContext = CreateViewContext(view); // Act and Assert @@ -344,13 +347,13 @@ foot-content"; { }); var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance(LayoutPath)) + pageFactory.Setup(p => p.CreateInstance(LayoutPath, false)) .Returns(layout); var view = new RazorView(pageFactory.Object, Mock.Of(), CreateViewStartProvider()); - view.Contextualize(page, isPartial: false); + view.Contextualize(page, isPartial: false, pageExecutionListener: null); var viewContext = CreateViewContext(view); // Act and Assert @@ -396,15 +399,15 @@ body-content"; v.RenderBodyPublic(); }); var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance("~/Shared/Layout1.cshtml")) + pageFactory.Setup(p => p.CreateInstance("~/Shared/Layout1.cshtml", false)) .Returns(layout1); - pageFactory.Setup(p => p.CreateInstance("~/Shared/Layout2.cshtml")) + pageFactory.Setup(p => p.CreateInstance("~/Shared/Layout2.cshtml", false)) .Returns(layout2); var view = new RazorView(pageFactory.Object, Mock.Of(), CreateViewStartProvider()); - view.Contextualize(page, isPartial: false); + view.Contextualize(page, isPartial: false, pageExecutionListener: null); var viewContext = CreateViewContext(view); // Act @@ -444,13 +447,13 @@ section-content-2"; }); var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance("layout-1")) + pageFactory.Setup(p => p.CreateInstance("layout-1", false)) .Returns(layout1); var view = new RazorView(pageFactory.Object, Mock.Of(), CreateViewStartProvider()); - view.Contextualize(page, isPartial: false); + view.Contextualize(page, isPartial: false, pageExecutionListener: null); var viewContext = CreateViewContext(view); // Act @@ -488,13 +491,13 @@ section-content-2"; }); var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance("layout-1")) + pageFactory.Setup(p => p.CreateInstance("layout-1", false)) .Returns(layout1); var view = new RazorView(pageFactory.Object, Mock.Of(), CreateViewStartProvider()); - view.Contextualize(page, isPartial: false); + view.Contextualize(page, isPartial: false, pageExecutionListener: null); var viewContext = CreateViewContext(view); // Act @@ -520,7 +523,7 @@ section-content-2"; var view = new RazorView(Mock.Of(), Mock.Of(), CreateViewStartProvider()); - view.Contextualize(page, isPartial: false); + view.Contextualize(page, isPartial: false, pageExecutionListener: null); var viewContext = CreateViewContext(view); // Act and Assert @@ -555,13 +558,13 @@ section-content-2"; v.Layout = "~/Shared/Layout2.cshtml"; }); var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance("~/Shared/Layout1.cshtml")) + pageFactory.Setup(p => p.CreateInstance("~/Shared/Layout1.cshtml", false)) .Returns(layout1); var view = new RazorView(pageFactory.Object, Mock.Of(), CreateViewStartProvider()); - view.Contextualize(page, isPartial: false); + view.Contextualize(page, isPartial: false, pageExecutionListener: null); var viewContext = CreateViewContext(view); // Act and Assert @@ -569,6 +572,153 @@ section-content-2"; Assert.Equal(expected, ex.Message); } + [Fact] + public async Task RenderAsync_UsesPageExecutionFeatureFromRequest_ToWrapWriter() + { + // Arrange + var pageWriter = CreateBufferedWriter(); + var layoutWriter = CreateBufferedWriter(); + + var layoutExecuted = false; + var count = -1; + var feature = new Mock(MockBehavior.Strict); + feature.Setup(f => f.DecorateWriter(It.IsAny())) + .Returns(() => + { + count++; + if (count == 0) + { + return pageWriter; + } + else if (count == 1) + { + return layoutWriter; + } + throw new Exception(); + }) + .Verifiable(); + + var pageContext = Mock.Of(); + feature.Setup(f => f.GetContext("/MyPage.cshtml", pageWriter)) + .Returns(pageContext) + .Verifiable(); + + var layoutContext = Mock.Of(); + feature.Setup(f => f.GetContext("/Layout.cshtml", layoutWriter)) + .Returns(layoutContext) + .Verifiable(); + + var page = new TestableRazorPage(v => + { + v.Layout = "/Layout.cshtml"; + Assert.Same(pageWriter, v.Output); + Assert.Same(pageContext, v.PageExecutionContext); + }); + page.Path = "/MyPage.cshtml"; + + var layout = new TestableRazorPage(v => + { + Assert.Same(layoutWriter, v.Output); + Assert.Same(layoutContext, v.PageExecutionContext); + v.RenderBodyPublic(); + + layoutExecuted = true; + }); + layout.Path = "/Layout.cshtml"; + + var pageFactory = new Mock(); + pageFactory.Setup(p => p.CreateInstance("/Layout.cshtml", true)) + .Returns(layout); + var viewStartProvider = new Mock(); + viewStartProvider.Setup(v => v.GetViewStartPages(It.IsAny(), true)) + .Returns(Enumerable.Empty()) + .Verifiable(); + var view = new RazorView(pageFactory.Object, + Mock.Of(), + viewStartProvider.Object); + view.Contextualize(page, isPartial: false, pageExecutionListener: feature.Object); + var viewContext = CreateViewContext(view); + + // Act + await view.RenderAsync(viewContext); + + // Assert + feature.Verify(); + viewStartProvider.Verify(); + Assert.True(layoutExecuted); + } + + [Fact] + public async Task RenderAsync_UsesPageExecutionFeatureFromRequest_ToGetExecutionContext() + { + // Arrange + var writer = Mock.Of(); + var executed = false; + var feature = new Mock(MockBehavior.Strict); + + var pageContext = Mock.Of(); + feature.Setup(f => f.GetContext("/MyPartialPage.cshtml", writer)) + .Returns(pageContext) + .Verifiable(); + + var page = new TestableRazorPage(v => + { + Assert.Same(writer, v.Output); + Assert.Same(pageContext, v.PageExecutionContext); + executed = true; + }); + page.Path = "/MyPartialPage.cshtml"; + + var view = new RazorView(Mock.Of(), + Mock.Of(), + Mock.Of()); + view.Contextualize(page, isPartial: true, pageExecutionListener: feature.Object); + var viewContext = CreateViewContext(view); + viewContext.Writer = writer; + + // Act + await view.RenderAsync(viewContext); + + // Assert + feature.Verify(); + Assert.True(executed); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task RenderAsync_DoesNotSetExecutionContextWhenListenerIsNotRegistered(bool isPartial) + { + // Arrange + var executed = false; + var page = new TestableRazorPage(v => + { + Assert.Null(v.PageExecutionContext); + executed = true; + }); + + var view = new RazorView(Mock.Of(), + Mock.Of(), + Mock.Of()); + view.Contextualize(page, isPartial, pageExecutionListener: null); + var viewContext = CreateViewContext(view); + + // Act + await view.RenderAsync(viewContext); + + // Assert + Assert.True(executed); + } + + private static TextWriter CreateBufferedWriter() + { + var mockWriter = new Mock(); + var bufferedWriter = mockWriter.As(); + bufferedWriter.SetupGet(b => b.IsBuffering) + .Returns(true); + return mockWriter.Object; + } + private static ViewContext CreateViewContext(RazorView view) { var httpContext = new DefaultHttpContext(); @@ -584,7 +734,7 @@ section-content-2"; { viewStartPages = viewStartPages ?? new IRazorPage[0]; var viewStartProvider = new Mock(); - viewStartProvider.Setup(v => v.GetViewStartPages(It.IsAny())) + viewStartProvider.Setup(v => v.GetViewStartPages(It.IsAny(), false)) .Returns(viewStartPages); return viewStartProvider.Object; diff --git a/test/WebSites/RazorInstrumentationWebsite/HomeController.cs b/test/WebSites/RazorInstrumentationWebsite/HomeController.cs new file mode 100644 index 0000000000..14a87a4c6e --- /dev/null +++ b/test/WebSites/RazorInstrumentationWebsite/HomeController.cs @@ -0,0 +1,21 @@ +// 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; +using Microsoft.AspNet.Mvc; + +namespace RazorInstrumentationWebSite +{ + public class HomeController : Controller + { + public ActionResult FullPath() + { + return View("/Views/Home/FullPath.cshtml"); + } + + public ActionResult ViewDiscoveryPath() + { + return View(); + } + } +} \ No newline at end of file diff --git a/test/WebSites/RazorInstrumentationWebsite/RazorInstrumentationWebsite.kproj b/test/WebSites/RazorInstrumentationWebsite/RazorInstrumentationWebsite.kproj new file mode 100644 index 0000000000..eaecc81cf4 --- /dev/null +++ b/test/WebSites/RazorInstrumentationWebsite/RazorInstrumentationWebsite.kproj @@ -0,0 +1,21 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 2b2b9876-903c-4065-8d62-2ee832bba106 + Library + + + + + + + + 2.0 + + + \ No newline at end of file diff --git a/test/WebSites/RazorInstrumentationWebsite/Startup.cs b/test/WebSites/RazorInstrumentationWebsite/Startup.cs new file mode 100644 index 0000000000..4f8987bd3a --- /dev/null +++ b/test/WebSites/RazorInstrumentationWebsite/Startup.cs @@ -0,0 +1,42 @@ +// 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; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.PageExecutionInstrumentation; +using Microsoft.Framework.DependencyInjection; + +namespace RazorInstrumentationWebSite +{ + public class Startup + { + public void Configure(IApplicationBuilder app) + { + var configuration = app.GetTestConfiguration(); + + // Set up application services + app.UseServices(services => + { + // Add MVC services to the services container + services.AddMvc(configuration); + }); + + app.Use(async (HttpContext context, Func next) => + { + if (!string.IsNullOrEmpty(context.Request.Headers["ENABLE-RAZOR-INSTRUMENTATION"])) + { + var pageExecutionContext = context.ApplicationServices.GetService(); + var listenerFeature = new TestPageExecutionListenerFeature(pageExecutionContext); + context.SetFeature(listenerFeature); + } + + await next(); + }); + + // Add MVC to the request pipeline + app.UseMvc(); + } + } +} diff --git a/test/WebSites/RazorInstrumentationWebsite/TestPageExecutionContext.cs b/test/WebSites/RazorInstrumentationWebsite/TestPageExecutionContext.cs new file mode 100644 index 0000000000..cd0eb02e31 --- /dev/null +++ b/test/WebSites/RazorInstrumentationWebsite/TestPageExecutionContext.cs @@ -0,0 +1,24 @@ +// 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; +using System.Collections.Generic; +using Microsoft.AspNet.PageExecutionInstrumentation; + +namespace RazorInstrumentationWebSite +{ + public class TestPageExecutionContext : IPageExecutionContext + { + public List> Values { get; } + = new List>(); + + public void BeginContext(int position, int length, bool isLiteral) + { + Values.Add(Tuple.Create(position, length, isLiteral)); + } + + public void EndContext() + { + } + } +} \ No newline at end of file diff --git a/test/WebSites/RazorInstrumentationWebsite/TestPageExecutionListenerFeature.cs b/test/WebSites/RazorInstrumentationWebsite/TestPageExecutionListenerFeature.cs new file mode 100644 index 0000000000..5f849c3058 --- /dev/null +++ b/test/WebSites/RazorInstrumentationWebsite/TestPageExecutionListenerFeature.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 System.IO; +using Microsoft.AspNet.PageExecutionInstrumentation; + +namespace RazorInstrumentationWebSite +{ + public class TestPageExecutionListenerFeature : IPageExecutionListenerFeature + { + private readonly IPageExecutionContext _context; + + public TestPageExecutionListenerFeature(IPageExecutionContext context) + { + _context = context; + } + + public TextWriter DecorateWriter(TextWriter writer) + { + return writer; + } + + public IPageExecutionContext GetContext(string sourceFilePath, TextWriter writer) + { + return _context; + } + } +} \ No newline at end of file diff --git a/test/WebSites/RazorInstrumentationWebsite/Views/Home/FullPath.cshtml b/test/WebSites/RazorInstrumentationWebsite/Views/Home/FullPath.cshtml new file mode 100644 index 0000000000..1622faf787 --- /dev/null +++ b/test/WebSites/RazorInstrumentationWebsite/Views/Home/FullPath.cshtml @@ -0,0 +1,3 @@ +

+page-content +

\ No newline at end of file diff --git a/test/WebSites/RazorInstrumentationWebsite/Views/Home/ViewDiscoveryPath.cshtml b/test/WebSites/RazorInstrumentationWebsite/Views/Home/ViewDiscoveryPath.cshtml new file mode 100644 index 0000000000..1622faf787 --- /dev/null +++ b/test/WebSites/RazorInstrumentationWebsite/Views/Home/ViewDiscoveryPath.cshtml @@ -0,0 +1,3 @@ +

+page-content +

\ No newline at end of file diff --git a/test/WebSites/RazorInstrumentationWebsite/Views/_Layout.cshtml b/test/WebSites/RazorInstrumentationWebsite/Views/_Layout.cshtml new file mode 100644 index 0000000000..d8615a7d7a --- /dev/null +++ b/test/WebSites/RazorInstrumentationWebsite/Views/_Layout.cshtml @@ -0,0 +1,4 @@ +
+@int.MaxValue +@RenderBody() +
\ No newline at end of file diff --git a/test/WebSites/RazorInstrumentationWebsite/Views/_ViewStart.cshtml b/test/WebSites/RazorInstrumentationWebsite/Views/_ViewStart.cshtml new file mode 100644 index 0000000000..3b3abdacaa --- /dev/null +++ b/test/WebSites/RazorInstrumentationWebsite/Views/_ViewStart.cshtml @@ -0,0 +1,5 @@ +@{ + Layout = "/Views/_Layout.cshtml"; + var viewStartMessage = "viewstart-content"; +} +@viewStartMessage diff --git a/test/WebSites/RazorInstrumentationWebsite/project.json b/test/WebSites/RazorInstrumentationWebsite/project.json new file mode 100644 index 0000000000..2112409be8 --- /dev/null +++ b/test/WebSites/RazorInstrumentationWebsite/project.json @@ -0,0 +1,11 @@ +{ + "dependencies": { + "Microsoft.AspNet.Mvc": "", + "Microsoft.AspNet.Server.IIS": "1.0.0-*", + "Microsoft.AspNet.Mvc.TestConfiguration": "" + }, + "frameworks": { + "aspnet50": { }, + "aspnetcore50": { } + } +}