From 687fd72efdeefb9d4445619840358c7544cc5286 Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Fri, 1 Apr 2016 17:08:57 -0700 Subject: [PATCH] Add Visual Studio specific RC1 binaries. - This is needed for Visual Studio RC1 backwards compatibility. --- Mvc.NoFun.sln | 17 +- NuGetPackageVerifier.json | 1 + .../Directives/ChunkHelper.cs | 93 ++++ .../Directives/ChunkInheritanceUtility.cs | 193 +++++++ .../Directives/ChunkTreeResult.cs | 46 ++ .../Directives/DefaultChunkTreeCache.cs | 77 +++ .../Directives/IChunkMerger.cs | 27 + .../Directives/IChunkTreeCache.cs | 27 + .../Directives/InjectChunkMerger.cs | 87 +++ .../Directives/SetBaseTypeChunkMerger.cs | 85 +++ .../Directives/UsingChunkMerger.cs | 56 ++ .../GeneratedTagHelperAttributeContext.cs | 22 + .../IMvcRazorHost.cs | 34 ++ .../InjectChunk.cs | 33 ++ .../InjectChunkVisitor.cs | 78 +++ .../InjectParameterGenerator.cs | 48 ++ .../Internal/DesignTimeRazorPathNormalizer.cs | 39 ++ .../Internal/RazorPathNormalizer.cs | 20 + ...icrosoft.AspNet.Mvc.Razor.Host.VSRC1.xproj | 17 + .../ModelChunk.cs | 27 + .../ModelChunkGenerator.cs | 40 ++ .../MvcCSharpChunkVisitor.cs | 42 ++ .../MvcCSharpCodeGenerator.cs | 128 +++++ .../MvcCSharpCodeVistor.cs | 31 ++ .../MvcCSharpDesignTimeCodeVisitor.cs | 70 +++ .../MvcRazorCodeParser.cs | 172 ++++++ .../MvcRazorHost.cs | 335 ++++++++++++ .../MvcRazorParser.cs | 229 ++++++++ .../MvcTagHelperAttributeValueCodeRenderer.cs | 92 ++++ .../Properties/AssemblyInfo.cs | 9 + .../Properties/Resources.Designer.cs | 126 +++++ .../PropertyHelper.cs | 513 ++++++++++++++++++ .../Resources.resx | 138 +++++ .../ViewHierarchyUtility.cs | 111 ++++ .../project.json | 29 + 35 files changed, 3091 insertions(+), 1 deletion(-) create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/ChunkHelper.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/ChunkInheritanceUtility.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/ChunkTreeResult.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/DefaultChunkTreeCache.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/IChunkMerger.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/IChunkTreeCache.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/InjectChunkMerger.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/SetBaseTypeChunkMerger.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/UsingChunkMerger.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/GeneratedTagHelperAttributeContext.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/IMvcRazorHost.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/InjectChunk.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/InjectChunkVisitor.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/InjectParameterGenerator.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Internal/DesignTimeRazorPathNormalizer.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Internal/RazorPathNormalizer.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Microsoft.AspNet.Mvc.Razor.Host.VSRC1.xproj create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/ModelChunk.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/ModelChunkGenerator.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcCSharpChunkVisitor.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcCSharpCodeGenerator.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcCSharpCodeVistor.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcCSharpDesignTimeCodeVisitor.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcRazorCodeParser.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcRazorHost.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcRazorParser.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcTagHelperAttributeValueCodeRenderer.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Properties/AssemblyInfo.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Properties/Resources.Designer.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/PropertyHelper.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Resources.resx create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/ViewHierarchyUtility.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/project.json diff --git a/Mvc.NoFun.sln b/Mvc.NoFun.sln index f8139aef7..d72a10339 100644 --- a/Mvc.NoFun.sln +++ b/Mvc.NoFun.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.24720.0 +VisualStudioVersion = 14.0.25123.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}" EndProject @@ -103,6 +103,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "MvcSubAreaSample.Web", "sam EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Mvc.Dnx", "src\Microsoft.AspNetCore.Mvc.Dnx\Microsoft.AspNetCore.Mvc.Dnx.xproj", "{8FB691C2-DFD8-4FEE-9628-2BB8466A691C}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.Razor.Host.VSRC1", "src\Microsoft.AspNet.Mvc.Razor.Host.VSRC1\Microsoft.AspNet.Mvc.Razor.Host.VSRC1.xproj", "{85C54A84-3E60-40E1-BE39-C2F514DD922E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -619,6 +621,18 @@ Global {8FB691C2-DFD8-4FEE-9628-2BB8466A691C}.Release|Mixed Platforms.Build.0 = Release|Any CPU {8FB691C2-DFD8-4FEE-9628-2BB8466A691C}.Release|x86.ActiveCfg = Release|Any CPU {8FB691C2-DFD8-4FEE-9628-2BB8466A691C}.Release|x86.Build.0 = Release|Any CPU + {85C54A84-3E60-40E1-BE39-C2F514DD922E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85C54A84-3E60-40E1-BE39-C2F514DD922E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85C54A84-3E60-40E1-BE39-C2F514DD922E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {85C54A84-3E60-40E1-BE39-C2F514DD922E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {85C54A84-3E60-40E1-BE39-C2F514DD922E}.Debug|x86.ActiveCfg = Debug|Any CPU + {85C54A84-3E60-40E1-BE39-C2F514DD922E}.Debug|x86.Build.0 = Debug|Any CPU + {85C54A84-3E60-40E1-BE39-C2F514DD922E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85C54A84-3E60-40E1-BE39-C2F514DD922E}.Release|Any CPU.Build.0 = Release|Any CPU + {85C54A84-3E60-40E1-BE39-C2F514DD922E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {85C54A84-3E60-40E1-BE39-C2F514DD922E}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {85C54A84-3E60-40E1-BE39-C2F514DD922E}.Release|x86.ActiveCfg = Release|Any CPU + {85C54A84-3E60-40E1-BE39-C2F514DD922E}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -668,5 +682,6 @@ Global {EE0BD773-4D47-4AA8-8472-5A938A3953BA} = {DAAE4C74-D06F-4874-A166-33305D2643CE} {45F6B3B6-D114-4D77-84D6-561B3957F341} = {DAAE4C74-D06F-4874-A166-33305D2643CE} {8FB691C2-DFD8-4FEE-9628-2BB8466A691C} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} + {85C54A84-3E60-40E1-BE39-C2F514DD922E} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} EndGlobalSection EndGlobal diff --git a/NuGetPackageVerifier.json b/NuGetPackageVerifier.json index 392bb1a74..322a420f7 100644 --- a/NuGetPackageVerifier.json +++ b/NuGetPackageVerifier.json @@ -16,6 +16,7 @@ "Microsoft.AspNetCore.Mvc.Localization": { }, "Microsoft.AspNetCore.Mvc.Razor": { }, "Microsoft.AspNetCore.Mvc.Razor.Host": { }, + "Microsoft.AspNet.Mvc.Razor.Host.VSRC1": { }, "Microsoft.AspNetCore.Mvc.TagHelpers": { }, "Microsoft.AspNetCore.Mvc.ViewFeatures": { }, "Microsoft.AspNetCore.Mvc.WebApiCompatShim": { } diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/ChunkHelper.cs b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/ChunkHelper.cs new file mode 100644 index 000000000..8f39c3596 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/ChunkHelper.cs @@ -0,0 +1,93 @@ +// 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.Linq; +using Microsoft.AspNet.Razor.Chunks; + +namespace Microsoft.AspNet.Mvc.Razor.Directives +{ + /// + /// Contains helper methods for dealing with Chunks + /// + public static class ChunkHelper + { + /// + /// Token that is replaced by the model name in @inherits and @inject + /// chunks as part of . + /// + public static readonly string TModelToken = "TModel"; + private static readonly string TModelReplaceToken = $"<{TModelToken}>"; + + /// + /// Returns the used to determine the model name for the page generated + /// using the specified + /// + /// The to scan for s in. + /// The last in the if found, null otherwise. + /// + public static ModelChunk GetModelChunk(ChunkTree chunkTree) + { + if (chunkTree == null) + { + throw new ArgumentNullException(nameof(chunkTree)); + } + + // If there's more than 1 model chunk there will be a Razor error BUT we want intellisense to show up on + // the current model chunk that the user is typing. + return chunkTree + .Chunks + .OfType() + .LastOrDefault(); + } + + /// + /// Returns the type name of the Model specified via a in the + /// if specified or the default model type. + /// + /// The to scan for s in. + /// The name of the default model. + /// The model type name for the generated page. + public static string GetModelTypeName( + ChunkTree chunkTree, + string defaultModelName) + { + if (chunkTree == null) + { + throw new ArgumentNullException(nameof(chunkTree)); + } + + if (defaultModelName == null) + { + throw new ArgumentNullException(nameof(defaultModelName)); + } + + var modelChunk = GetModelChunk(chunkTree); + return modelChunk != null ? modelChunk.ModelType : defaultModelName; + } + + /// + /// Returns a string with the <TModel> token replaced with the value specified in + /// . + /// + /// The string to replace the token in. + /// The model name to replace with. + /// A string with the token replaced. + public static string ReplaceTModel( + string value, + string modelName) + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + if (modelName == null) + { + throw new ArgumentNullException(nameof(modelName)); + } + + return value.Replace(TModelReplaceToken, modelName); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/ChunkInheritanceUtility.cs b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/ChunkInheritanceUtility.cs new file mode 100644 index 000000000..4b77170f9 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/ChunkInheritanceUtility.cs @@ -0,0 +1,193 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.AspNet.FileProviders.VSRC1; +using Microsoft.AspNet.Razor; +using Microsoft.AspNet.Razor.Chunks; +using Microsoft.AspNet.Razor.Parser; + +namespace Microsoft.AspNet.Mvc.Razor.Directives +{ + /// + /// A utility type for supporting inheritance of directives into a page from applicable _ViewImports pages. + /// + public class ChunkInheritanceUtility + { + private readonly MvcRazorHost _razorHost; + private readonly IReadOnlyList _defaultInheritedChunks; + private readonly IChunkTreeCache _chunkTreeCache; + + /// + /// Initializes a new instance of . + /// + /// The used to parse _ViewImports pages. + /// that caches instances. + /// + /// Sequence of s inherited by default. + public ChunkInheritanceUtility( + MvcRazorHost razorHost, + IChunkTreeCache chunkTreeCache, + IReadOnlyList defaultInheritedChunks) + { + if (razorHost == null) + { + throw new ArgumentNullException(nameof(razorHost)); + } + + if (chunkTreeCache == null) + { + throw new ArgumentNullException(nameof(chunkTreeCache)); + } + + if (defaultInheritedChunks == null) + { + throw new ArgumentNullException(nameof(defaultInheritedChunks)); + } + + _razorHost = razorHost; + _defaultInheritedChunks = defaultInheritedChunks; + _chunkTreeCache = chunkTreeCache; + } + + /// + /// Gets an ordered of parsed s and + /// file paths for each _ViewImports that is applicable to the page located at + /// . The list is ordered so that the 's + /// for the _ViewImports closest to the + /// in the file system appears first. + /// + /// The path of the page to locate inherited chunks for. + /// A of parsed _ViewImports + /// s and their file paths. + /// + /// The resulting is ordered so that the result + /// for a _ViewImport closest to the application root appears first and the _ViewImport + /// closest to the page appears last i.e. + /// [ /_ViewImport, /Views/_ViewImport, /Views/Home/_ViewImport ] + /// + public virtual IReadOnlyList GetInheritedChunkTreeResults(string pagePath) + { + if (pagePath == null) + { + throw new ArgumentNullException(nameof(pagePath)); + } + + var inheritedChunkTreeResults = new List(); + var templateEngine = new RazorTemplateEngine(_razorHost); + foreach (var viewImportsPath in ViewHierarchyUtility.GetViewImportsLocations(pagePath)) + { + // viewImportsPath contains the app-relative path of the _ViewImports. + // Since the parsing of a _ViewImports would cause parent _ViewImports to be parsed + // we need to ensure the paths are app-relative to allow the GetGlobalFileLocations + // for the current _ViewImports to succeed. + var chunkTree = _chunkTreeCache.GetOrAdd( + viewImportsPath, + fileInfo => ParseViewFile( + templateEngine, + fileInfo, + viewImportsPath)); + + if (chunkTree != null) + { + var result = new ChunkTreeResult(chunkTree, viewImportsPath); + inheritedChunkTreeResults.Insert(0, result); + } + } + + return inheritedChunkTreeResults; + } + + /// + /// Merges inherited by default and instances produced by parsing + /// _ViewImports files into the specified . + /// + /// The to merge in to. + /// inherited from _ViewImports + /// files. + /// The default model name. + public void MergeInheritedChunkTrees( + ChunkTree chunkTree, + IReadOnlyList inheritedChunkTrees, + string defaultModel) + { + if (chunkTree == null) + { + throw new ArgumentNullException(nameof(chunkTree)); + } + + if (inheritedChunkTrees == null) + { + throw new ArgumentNullException(nameof(inheritedChunkTrees)); + } + + var chunkMergers = GetChunkMergers(chunkTree, defaultModel); + // We merge chunks into the ChunkTree in two passes. In the first pass, we traverse the ChunkTree visiting + // a mapped IChunkMerger for types that are registered. + foreach (var chunk in chunkTree.Chunks) + { + foreach (var merger in chunkMergers) + { + merger.VisitChunk(chunk); + } + } + + var inheritedChunks = _defaultInheritedChunks.Concat( + inheritedChunkTrees.SelectMany(tree => tree.Chunks)).ToArray(); + + foreach (var merger in chunkMergers) + { + merger.MergeInheritedChunks(chunkTree, inheritedChunks); + } + } + + private static IChunkMerger[] GetChunkMergers(ChunkTree chunkTree, string defaultModel) + { + var modelType = ChunkHelper.GetModelTypeName(chunkTree, defaultModel); + return new IChunkMerger[] + { + new UsingChunkMerger(), + new InjectChunkMerger(modelType), + new SetBaseTypeChunkMerger(modelType) + }; + } + + private static ChunkTree ParseViewFile( + RazorTemplateEngine engine, + IFileInfo fileInfo, + string viewImportsPath) + { + using (var stream = fileInfo.CreateReadStream()) + { + using (var streamReader = new StreamReader(stream)) + { + var parseResults = engine.ParseTemplate(streamReader, viewImportsPath); + var className = ParserHelpers.SanitizeClassName(fileInfo.Name); + var language = engine.Host.CodeLanguage; + var chunkGenerator = language.CreateChunkGenerator( + className, + engine.Host.DefaultNamespace, + viewImportsPath, + engine.Host); + chunkGenerator.Visit(parseResults); + + // Rewrite the location of inherited chunks so they point to the global import file. + var chunkTree = chunkGenerator.Context.ChunkTreeBuilder.ChunkTree; + foreach (var chunk in chunkTree.Chunks) + { + chunk.Start = new SourceLocation( + viewImportsPath, + chunk.Start.AbsoluteIndex, + chunk.Start.LineIndex, + chunk.Start.CharacterIndex); + } + + return chunkTree; + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/ChunkTreeResult.cs b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/ChunkTreeResult.cs new file mode 100644 index 000000000..1f96a130c --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/ChunkTreeResult.cs @@ -0,0 +1,46 @@ +// 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.AspNet.Razor.Chunks; + +namespace Microsoft.AspNet.Mvc.Razor.Directives +{ + /// + /// Contains information. + /// + public class ChunkTreeResult + { + /// + /// Initializes a new instance of . + /// + /// The generated from the file at the + /// given . + /// The path to the file that generated the given . + public ChunkTreeResult(ChunkTree chunkTree, string filePath) + { + if (chunkTree == null) + { + throw new ArgumentNullException(nameof(chunkTree)); + } + + if (filePath == null) + { + throw new ArgumentNullException(nameof(filePath)); + } + + ChunkTree = chunkTree; + FilePath = filePath; + } + + /// + /// The generated from the file at . + /// + public ChunkTree ChunkTree { get; } + + /// + /// The path to the file that generated the . + /// + public string FilePath { get; } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/DefaultChunkTreeCache.cs b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/DefaultChunkTreeCache.cs new file mode 100644 index 000000000..8a61b354d --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/DefaultChunkTreeCache.cs @@ -0,0 +1,77 @@ +// 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.AspNet.FileProviders.VSRC1; +using Microsoft.AspNet.Razor.Chunks; +using Microsoft.Extensions.Caching.Memory.VSRC1; + +namespace Microsoft.AspNet.Mvc.Razor.Directives +{ + /// + /// Default implementation of . + /// + public class DefaultChunkTreeCache : IChunkTreeCache + { + private static readonly MemoryCacheOptions MemoryCacheOptions = new MemoryCacheOptions + { + CompactOnMemoryPressure = false + }; + private static readonly TimeSpan SlidingExpirationDuration = TimeSpan.FromMinutes(1); + private readonly IFileProvider _fileProvider; + private readonly IMemoryCache _chunkTreeCache; + + /// + /// Initializes a new instance of . + /// + /// The application's . + public DefaultChunkTreeCache(IFileProvider fileProvider) + : this(fileProvider, MemoryCacheOptions) + { + } + + // Internal for unit testing + internal DefaultChunkTreeCache( + IFileProvider fileProvider, + MemoryCacheOptions options) + { + _fileProvider = fileProvider; + _chunkTreeCache = new MemoryCache(options); + } + + /// + public ChunkTree GetOrAdd( + string pagePath, + Func getChunkTree) + { + if (pagePath == null) + { + throw new ArgumentNullException(nameof(pagePath)); + } + + if (getChunkTree == null) + { + throw new ArgumentNullException(nameof(getChunkTree)); + } + + ChunkTree chunkTree; + if (!_chunkTreeCache.TryGetValue(pagePath, out chunkTree)) + { + // GetOrAdd is invoked for each _ViewImport that might potentially exist in the path. + // We can avoid performing file system lookups for files that do not exist by caching + // negative results and adding a Watch for that file. + + var options = new MemoryCacheEntryOptions() + .AddExpirationToken(_fileProvider.Watch(pagePath)) + .SetSlidingExpiration(SlidingExpirationDuration); + + var file = _fileProvider.GetFileInfo(pagePath); + chunkTree = file.Exists ? getChunkTree(file) : null; + + _chunkTreeCache.Set(pagePath, chunkTree, options); + } + + return chunkTree; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/IChunkMerger.cs b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/IChunkMerger.cs new file mode 100644 index 000000000..fbef21fcf --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/IChunkMerger.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNet.Razor.Chunks; + +namespace Microsoft.AspNet.Mvc.Razor.Directives +{ + /// + /// Defines the contract for merging instances from _ViewStart files. + /// + public interface IChunkMerger + { + /// + /// Visits a from the to merge into. + /// + /// A from the tree. + void VisitChunk(Chunk chunk); + + /// + /// Merges an inherited into the . + /// + /// The to merge into. + /// The s to merge. + void MergeInheritedChunks(ChunkTree chunkTree, IReadOnlyList inheritedChunks); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/IChunkTreeCache.cs b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/IChunkTreeCache.cs new file mode 100644 index 000000000..58d854ace --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/IChunkTreeCache.cs @@ -0,0 +1,27 @@ +// 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.AspNet.FileProviders.VSRC1; +using Microsoft.AspNet.Razor.Chunks; + +namespace Microsoft.AspNet.Mvc.Razor.Directives +{ + /// + /// A cache for parsed s. + /// + public interface IChunkTreeCache + { + /// + /// Get an existing , or create and add a new one if it is + /// not available in the cache or is expired. + /// + /// The application relative path of the Razor page. + /// A delegate that creates a new . + /// The if a file exists at , + /// null otherwise. + /// The resulting does not contain inherited chunks from _ViewStart or + /// default inherited chunks. + ChunkTree GetOrAdd(string pagePath, Func getChunkTree); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/InjectChunkMerger.cs b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/InjectChunkMerger.cs new file mode 100644 index 000000000..99f835453 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/InjectChunkMerger.cs @@ -0,0 +1,87 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.Razor.Chunks; + +namespace Microsoft.AspNet.Mvc.Razor.Directives +{ + /// + /// A that merges instances. + /// + public class InjectChunkMerger : IChunkMerger + { + private readonly HashSet _addedMemberNames = new HashSet(StringComparer.Ordinal); + private string _modelType; + + /// + /// Initializes a new instance of . + /// + /// The model type to be used to replace <TModel> tokens. + public InjectChunkMerger(string modelType) + { + if (modelType == null) + { + throw new ArgumentNullException(nameof(modelType)); + } + + _modelType = "<" + modelType + ">"; + } + + /// + public void VisitChunk(Chunk chunk) + { + if (chunk == null) + { + throw new ArgumentNullException(nameof(chunk)); + } + + var injectChunk = chunk as InjectChunk; + if (injectChunk != null) + { + injectChunk.TypeName = ChunkHelper.ReplaceTModel(injectChunk.TypeName, _modelType); + _addedMemberNames.Add(injectChunk.MemberName); + } + } + + /// + public void MergeInheritedChunks(ChunkTree chunkTree, IReadOnlyList inheritedChunks) + { + if (chunkTree == null) + { + throw new ArgumentNullException(nameof(chunkTree)); + } + + if (inheritedChunks == null) + { + throw new ArgumentNullException(nameof(inheritedChunks)); + } + + for (var i = inheritedChunks.Count - 1; i >= 0; i--) + { + var injectChunk = inheritedChunks[i] as InjectChunk; + if (injectChunk != null && + _addedMemberNames.Add(injectChunk.MemberName)) + { + chunkTree.Chunks.Add(TransformChunk(injectChunk)); + } + } + } + + private InjectChunk TransformChunk(InjectChunk injectChunk) + { + var typeName = ChunkHelper.ReplaceTModel(injectChunk.TypeName, _modelType); + if (typeName != injectChunk.TypeName) + { + return new InjectChunk(typeName, injectChunk.MemberName) + { + Start = injectChunk.Start, + Association = injectChunk.Association + }; + } + return injectChunk; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/SetBaseTypeChunkMerger.cs b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/SetBaseTypeChunkMerger.cs new file mode 100644 index 000000000..05ab57f9a --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/SetBaseTypeChunkMerger.cs @@ -0,0 +1,85 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNet.Razor.Chunks; + +namespace Microsoft.AspNet.Mvc.Razor.Directives +{ + /// + /// A that merges instances. + /// + public class SetBaseTypeChunkMerger : IChunkMerger + { + private readonly string _modelType; + private bool _isBaseTypeSet; + + /// + /// Initializes a new instance of . + /// + /// The type name of the model used by default. + public SetBaseTypeChunkMerger(string modelType) + { + _modelType = "<" + modelType + ">"; + } + + /// + public void VisitChunk(Chunk chunk) + { + if (chunk == null) + { + throw new ArgumentNullException(nameof(chunk)); + } + + var setBaseTypeChunk = chunk as SetBaseTypeChunk; + if (setBaseTypeChunk != null) + { + setBaseTypeChunk.TypeName = ChunkHelper.ReplaceTModel(setBaseTypeChunk.TypeName, _modelType); + _isBaseTypeSet = true; + } + } + + /// + public void MergeInheritedChunks(ChunkTree chunkTree, IReadOnlyList inheritedChunks) + { + if (chunkTree == null) + { + throw new ArgumentNullException(nameof(chunkTree)); + } + + if (inheritedChunks == null) + { + throw new ArgumentNullException(nameof(inheritedChunks)); + } + + if (!_isBaseTypeSet) + { + for (var i = inheritedChunks.Count - 1; i >= 0; i--) + { + var baseTypeChunk = inheritedChunks[i] as SetBaseTypeChunk; + if (baseTypeChunk != null) + { + chunkTree.Chunks.Add(TransformChunk(baseTypeChunk)); + break; + } + } + } + } + + private SetBaseTypeChunk TransformChunk(SetBaseTypeChunk setBaseTypeChunk) + { + var typeName = ChunkHelper.ReplaceTModel(setBaseTypeChunk.TypeName, _modelType); + if (typeName != setBaseTypeChunk.TypeName) + { + return new SetBaseTypeChunk + { + TypeName = typeName, + Start = setBaseTypeChunk.Start, + Association = setBaseTypeChunk.Association + }; + } + return setBaseTypeChunk; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/UsingChunkMerger.cs b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/UsingChunkMerger.cs new file mode 100644 index 000000000..896aae6f7 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Directives/UsingChunkMerger.cs @@ -0,0 +1,56 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.Razor.Chunks; + +namespace Microsoft.AspNet.Mvc.Razor.Directives +{ + /// + /// A that merges instances. + /// + public class UsingChunkMerger : IChunkMerger + { + private readonly HashSet _currentUsings = new HashSet(StringComparer.Ordinal); + + /// + public void VisitChunk(Chunk chunk) + { + if (chunk == null) + { + throw new ArgumentNullException(nameof(chunk)); + } + + var namespaceChunk = chunk as UsingChunk; + if (namespaceChunk != null) + { + _currentUsings.Add(namespaceChunk.Namespace); + } + } + + /// + public void MergeInheritedChunks(ChunkTree chunkTree, IReadOnlyList inheritedChunks) + { + if (chunkTree == null) + { + throw new ArgumentNullException(nameof(chunkTree)); + } + + if (inheritedChunks == null) + { + throw new ArgumentNullException(nameof(inheritedChunks)); + } + + var namespaceChunks = inheritedChunks.OfType(); + foreach (var namespaceChunk in namespaceChunks) + { + if (_currentUsings.Add(namespaceChunk.Namespace)) + { + chunkTree.Chunks.Add(namespaceChunk); + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/GeneratedTagHelperAttributeContext.cs b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/GeneratedTagHelperAttributeContext.cs new file mode 100644 index 000000000..a0cfa398b --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/GeneratedTagHelperAttributeContext.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Mvc.Razor +{ + /// + /// Contains information for the attribute code + /// generation process. + /// + public class GeneratedTagHelperAttributeContext + { + /// + /// Name of the model expression type. + /// + public string ModelExpressionTypeName { get; set; } + + /// + /// Name the method to create ModelExpressions. + /// + public string CreateModelExpressionMethodName { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/IMvcRazorHost.cs b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/IMvcRazorHost.cs new file mode 100644 index 000000000..ad2b7ef85 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/IMvcRazorHost.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using Microsoft.AspNet.Razor.CodeGenerators; + +namespace Microsoft.AspNet.Mvc.Razor +{ + /// + /// Specifies the contracts for a Razor host that parses Razor files and generates C# code. + /// + public interface IMvcRazorHost + { + /// + /// 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); + + /// + /// Represent the prefix off the main entry class in the view. + /// + string MainClassNamePrefix { get; } + + /// + /// Represent the namespace the main entry class in the view. + /// + string DefaultNamespace { get; } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/InjectChunk.cs b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/InjectChunk.cs new file mode 100644 index 000000000..fccb761bf --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/InjectChunk.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Chunks; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public class InjectChunk : Chunk + { + /// + /// Represents the chunk for an @inject statement. + /// + /// The type name of the property to be injected + /// The member name of the property to be injected. + public InjectChunk( + string typeName, + string propertyName) + { + TypeName = typeName; + MemberName = propertyName; + } + + /// + /// Gets or sets the type name of the property to be injected. + /// + public string TypeName { get; set; } + + /// + /// Gets or sets the name of the property to be injected. + /// + public string MemberName { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/InjectChunkVisitor.cs b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/InjectChunkVisitor.cs new file mode 100644 index 000000000..bbd37b567 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/InjectChunkVisitor.cs @@ -0,0 +1,78 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNet.Razor.CodeGenerators; +using Microsoft.AspNet.Razor.CodeGenerators.Visitors; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public class InjectChunkVisitor : MvcCSharpCodeVisitor + { + private readonly string _injectAttribute; + + public InjectChunkVisitor( + CSharpCodeWriter writer, + CodeGeneratorContext context, + string injectAttributeName) + : base(writer, context) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (injectAttributeName == null) + { + throw new ArgumentNullException(nameof(injectAttributeName)); + } + + _injectAttribute = "[" + injectAttributeName + "]"; + } + + public IList InjectChunks { get; } = new List(); + + protected override void Visit(InjectChunk chunk) + { + if (chunk == null) + { + throw new ArgumentNullException(nameof(chunk)); + } + + Writer.WriteLine(_injectAttribute); + + // Some of the chunks that we visit are either InjectDescriptors that are added by default or + // are chunks from _ViewStart files and are not associated with any Spans. Invoking + // CreateExpressionMapping to produce line mappings on these chunks would fail. We'll skip + // generating code mappings for these chunks. This makes sense since the chunks do not map + // to any code in the current view. + if (Context.Host.DesignTimeMode && chunk.Association != null) + { + Writer.WriteLine("public"); + + var code = string.IsNullOrEmpty(chunk.MemberName) ? + chunk.TypeName : + chunk.TypeName + " " + chunk.MemberName; + var csharpVisitor = new CSharpCodeVisitor(Writer, Context); + csharpVisitor.CreateExpressionCodeMapping(code, chunk); + Writer.WriteLine("{ get; private set; }"); + } + else + { + Writer.Write("public ") + .Write(chunk.TypeName) + .Write(" ") + .Write(chunk.MemberName) + .WriteLine(" { get; private set; }"); + } + + InjectChunks.Add(chunk); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/InjectParameterGenerator.cs b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/InjectParameterGenerator.cs new file mode 100644 index 000000000..a2ed9c068 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/InjectParameterGenerator.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; +using System.Globalization; +using Microsoft.AspNet.Razor.Chunks.Generators; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public class InjectParameterGenerator : SpanChunkGenerator + { + public InjectParameterGenerator(string typeName, string propertyName) + { + TypeName = typeName; + PropertyName = propertyName; + } + + public string TypeName { get; private set; } + + public string PropertyName { get; private set; } + + public override void GenerateChunk(Span target, ChunkGeneratorContext context) + { + var injectChunk = new InjectChunk(TypeName, PropertyName); + context.ChunkTreeBuilder.AddChunk(injectChunk, target); + } + + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "@inject {0} {1}", TypeName, PropertyName); + } + + public override bool Equals(object obj) + { + var other = obj as InjectParameterGenerator; + return other != null && + string.Equals(TypeName, other.TypeName, StringComparison.Ordinal) && + string.Equals(PropertyName, other.PropertyName, StringComparison.Ordinal); + } + + public override int GetHashCode() + { + return TypeName.GetHashCode() + + (PropertyName.GetHashCode() * 13); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Internal/DesignTimeRazorPathNormalizer.cs b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Internal/DesignTimeRazorPathNormalizer.cs new file mode 100644 index 000000000..8841859d5 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Internal/DesignTimeRazorPathNormalizer.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; + +namespace Microsoft.AspNet.Mvc.Razor.Internal +{ + public class DesignTimeRazorPathNormalizer : RazorPathNormalizer + { + private readonly string _applicationRoot; + + public DesignTimeRazorPathNormalizer(string applicationRoot) + { + if (applicationRoot == null) + { + throw new ArgumentNullException(nameof(applicationRoot)); + } + + _applicationRoot = applicationRoot; + } + + public override string NormalizePath(string path) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + // Need to convert path to application relative (rooted paths are passed in during design time). + if (Path.IsPathRooted(path) && path.StartsWith(_applicationRoot, StringComparison.Ordinal)) + { + path = path.Substring(_applicationRoot.Length); + } + + return path; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Internal/RazorPathNormalizer.cs b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Internal/RazorPathNormalizer.cs new file mode 100644 index 000000000..21cfc2b3c --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Internal/RazorPathNormalizer.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNet.Mvc.Razor.Internal +{ + public class RazorPathNormalizer + { + public virtual string NormalizePath(string path) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + return path; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Microsoft.AspNet.Mvc.Razor.Host.VSRC1.xproj b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Microsoft.AspNet.Mvc.Razor.Host.VSRC1.xproj new file mode 100644 index 000000000..e3e003061 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Microsoft.AspNet.Mvc.Razor.Host.VSRC1.xproj @@ -0,0 +1,17 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 85c54a84-3e60-40e1-be39-c2f514dd922e + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/ModelChunk.cs b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/ModelChunk.cs new file mode 100644 index 000000000..1c86d091f --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/ModelChunk.cs @@ -0,0 +1,27 @@ +// 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.AspNet.Razor.Chunks; + +namespace Microsoft.AspNet.Mvc.Razor +{ + /// + /// for an @model directive. + /// + public class ModelChunk : Chunk + { + /// + /// Initializes a new instance of . + /// + /// The type of the view's model. + public ModelChunk(string modelType) + { + ModelType = modelType; + } + + /// + /// Gets the type of the view's model. + /// + public string ModelType { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/ModelChunkGenerator.cs b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/ModelChunkGenerator.cs new file mode 100644 index 000000000..55e55920f --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/ModelChunkGenerator.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.AspNet.Mvc.Razor; +using Microsoft.AspNet.Razor.Chunks.Generators; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Generator +{ + public class ModelChunkGenerator : SpanChunkGenerator + { + public ModelChunkGenerator(string modelType) + { + ModelType = modelType; + } + + public string ModelType { get; } + + public override void GenerateChunk(Span target, ChunkGeneratorContext context) + { + var modelChunk = new ModelChunk(ModelType); + context.ChunkTreeBuilder.AddChunk(modelChunk, target, topLevel: true); + } + + public override string ToString() => ModelType; + + public override bool Equals(object obj) + { + var other = obj as ModelChunkGenerator; + return other != null && + string.Equals(ModelType, other.ModelType, StringComparison.Ordinal); + } + + public override int GetHashCode() + { + return StringComparer.Ordinal.GetHashCode(ModelType); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcCSharpChunkVisitor.cs b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcCSharpChunkVisitor.cs new file mode 100644 index 000000000..e90bbc70b --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcCSharpChunkVisitor.cs @@ -0,0 +1,42 @@ +// 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.AspNet.Razor.Chunks; +using Microsoft.AspNet.Razor.CodeGenerators; +using Microsoft.AspNet.Razor.CodeGenerators.Visitors; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public abstract class MvcCSharpChunkVisitor : CodeVisitor + { + public MvcCSharpChunkVisitor( + CSharpCodeWriter writer, + CodeGeneratorContext context) + : base(writer, context) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + } + + public override void Accept(Chunk chunk) + { + if (chunk is InjectChunk) + { + Visit((InjectChunk)chunk); + } + else + { + base.Accept(chunk); + } + } + + protected abstract void Visit(InjectChunk chunk); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcCSharpCodeGenerator.cs b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcCSharpCodeGenerator.cs new file mode 100644 index 000000000..78b6fba6d --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcCSharpCodeGenerator.cs @@ -0,0 +1,128 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using Microsoft.AspNet.Mvc.Razor.Directives; +using Microsoft.AspNet.Razor.CodeGenerators; +using Microsoft.AspNet.Razor.CodeGenerators.Visitors; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public class MvcCSharpCodeGenerator : CSharpCodeGenerator + { + private readonly GeneratedTagHelperAttributeContext _tagHelperAttributeContext; + private readonly string _defaultModel; + private readonly string _injectAttribute; + + public MvcCSharpCodeGenerator( + CodeGeneratorContext context, + string defaultModel, + string injectAttribute, + GeneratedTagHelperAttributeContext tagHelperAttributeContext) + : base(context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (defaultModel == null) + { + throw new ArgumentNullException(nameof(defaultModel)); + } + + if (injectAttribute == null) + { + throw new ArgumentNullException(nameof(injectAttribute)); + } + + if (tagHelperAttributeContext == null) + { + throw new ArgumentNullException(nameof(tagHelperAttributeContext)); + } + + _tagHelperAttributeContext = tagHelperAttributeContext; + _defaultModel = defaultModel; + _injectAttribute = injectAttribute; + } + + protected override CSharpCodeWritingScope BuildClassDeclaration(CSharpCodeWriter writer) + { + if (Context.Host.DesignTimeMode && + string.Equals( + Path.GetFileName(Context.SourceFile), + ViewHierarchyUtility.ViewImportsFileName, + StringComparison.OrdinalIgnoreCase)) + { + // Write a using TModel = System.Object; token during design time to make intellisense work + writer.WriteLine($"using {ChunkHelper.TModelToken} = {typeof(object).FullName};"); + } + + return base.BuildClassDeclaration(writer); + } + + protected override CSharpCodeVisitor CreateCSharpCodeVisitor( + CSharpCodeWriter writer, + CodeGeneratorContext context) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var csharpCodeVisitor = base.CreateCSharpCodeVisitor(writer, context); + + csharpCodeVisitor.TagHelperRenderer.AttributeValueCodeRenderer = + new MvcTagHelperAttributeValueCodeRenderer(_tagHelperAttributeContext); + + return csharpCodeVisitor; + } + + protected override CSharpDesignTimeCodeVisitor CreateCSharpDesignTimeCodeVisitor( + CSharpCodeVisitor csharpCodeVisitor, + CSharpCodeWriter writer, + CodeGeneratorContext context) + { + if (csharpCodeVisitor == null) + { + throw new ArgumentNullException(nameof(csharpCodeVisitor)); + } + + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new MvcCSharpDesignTimeCodeVisitor(csharpCodeVisitor, writer, context); + } + + protected override void BuildConstructor(CSharpCodeWriter writer) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + base.BuildConstructor(writer); + + writer.WriteLineHiddenDirective(); + + var injectVisitor = new InjectChunkVisitor(writer, Context, _injectAttribute); + injectVisitor.Accept(Context.ChunkTreeBuilder.ChunkTree.Chunks); + + writer.WriteLine(); + writer.WriteLineHiddenDirective(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcCSharpCodeVistor.cs b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcCSharpCodeVistor.cs new file mode 100644 index 000000000..77169dc99 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcCSharpCodeVistor.cs @@ -0,0 +1,31 @@ +// 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.AspNet.Razor.CodeGenerators; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public abstract class MvcCSharpCodeVisitor : MvcCSharpChunkVisitor + { + public MvcCSharpCodeVisitor( + CSharpCodeWriter writer, + CodeGeneratorContext context) + : base(writer, context) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + } + + protected override void Visit(InjectChunk chunk) + { + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcCSharpDesignTimeCodeVisitor.cs b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcCSharpDesignTimeCodeVisitor.cs new file mode 100644 index 000000000..e2db2dfc5 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcCSharpDesignTimeCodeVisitor.cs @@ -0,0 +1,70 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Diagnostics; +using Microsoft.AspNet.Razor.Chunks; +using Microsoft.AspNet.Razor.CodeGenerators; +using Microsoft.AspNet.Razor.CodeGenerators.Visitors; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public class MvcCSharpDesignTimeCodeVisitor : CSharpDesignTimeCodeVisitor + { + private const string ModelVariable = "__modelHelper"; + private ModelChunk _modelChunk; + + public MvcCSharpDesignTimeCodeVisitor( + CSharpCodeVisitor csharpCodeVisitor, + CSharpCodeWriter writer, + CodeGeneratorContext context) + : base(csharpCodeVisitor, writer, context) + { + } + + protected override void AcceptTreeCore(ChunkTree tree) + { + base.AcceptTreeCore(tree); + + if (_modelChunk != null) + { + WriteModelChunkLineMapping(); + } + } + + public override void Accept(Chunk chunk) + { + if (chunk is ModelChunk) + { + Visit((ModelChunk)chunk); + } + + base.Accept(chunk); + } + + private void Visit(ModelChunk chunk) + { + Debug.Assert(chunk != null); + _modelChunk = chunk; + } + + private void WriteModelChunkLineMapping() + { + Debug.Assert(Context.Host.DesignTimeMode); + + using (var lineMappingWriter = + Writer.BuildLineMapping(_modelChunk.Start, _modelChunk.ModelType.Length, Context.SourceFile)) + { + // var __modelHelper = default(MyModel); + Writer.Write("var ") + .Write(ModelVariable) + .Write(" = default("); + + lineMappingWriter.MarkLineMappingStart(); + Writer.Write(_modelChunk.ModelType); + lineMappingWriter.MarkLineMappingEnd(); + + Writer.WriteLine(");"); + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcRazorCodeParser.cs b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcRazorCodeParser.cs new file mode 100644 index 000000000..252ea60ad --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcRazorCodeParser.cs @@ -0,0 +1,172 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Diagnostics; +using Microsoft.AspNet.Mvc.Razor.Host; +using Microsoft.AspNet.Razor; +using Microsoft.AspNet.Razor.Chunks.Generators; +using Microsoft.AspNet.Razor.Generator; +using Microsoft.AspNet.Razor.Parser; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public class MvcRazorCodeParser : CSharpCodeParser + { + private const string ModelKeyword = "model"; + private const string InjectKeyword = "inject"; + private SourceLocation? _endInheritsLocation; + private bool _modelStatementFound; + + public MvcRazorCodeParser() + { + MapDirectives(ModelDirective, ModelKeyword); + MapDirectives(InjectDirective, InjectKeyword); + } + + protected override void InheritsDirective() + { + // Verify we're on the right keyword and accept + AssertDirective(SyntaxConstants.CSharp.InheritsKeyword); + AcceptAndMoveNext(); + _endInheritsLocation = CurrentLocation; + + InheritsDirectiveCore(); + CheckForInheritsAndModelStatements(); + } + + private void CheckForInheritsAndModelStatements() + { + if (_modelStatementFound && _endInheritsLocation.HasValue) + { + Context.OnError( + _endInheritsLocation.Value, + Resources.FormatMvcRazorCodeParser_CannotHaveModelAndInheritsKeyword(ModelKeyword), + SyntaxConstants.CSharp.InheritsKeyword.Length); + } + } + + protected virtual void ModelDirective() + { + // Verify we're on the right keyword and accept + AssertDirective(ModelKeyword); + var startModelLocation = CurrentLocation; + AcceptAndMoveNext(); + + + BaseTypeDirective(Resources.FormatMvcRazorCodeParser_KeywordMustBeFollowedByTypeName(ModelKeyword), + CreateModelChunkGenerator); + + if (_modelStatementFound) + { + Context.OnError( + startModelLocation, + Resources.FormatMvcRazorCodeParser_OnlyOneModelStatementIsAllowed(ModelKeyword), + ModelKeyword.Length); + } + + _modelStatementFound = true; + + CheckForInheritsAndModelStatements(); + } + + protected virtual void InjectDirective() + { + // @inject MyApp.MyService MyServicePropertyName + AssertDirective(InjectKeyword); + var startLocation = CurrentLocation; + AcceptAndMoveNext(); + + Context.CurrentBlock.Type = BlockType.Directive; + + // Accept whitespace + var remainingWhitespace = AcceptSingleWhiteSpaceCharacter(); + var keywordwithSingleWhitespaceLength = Span.GetContent().Value.Length; + if (Span.Symbols.Count > 1) + { + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + } + Output(SpanKind.MetaCode); + + if (remainingWhitespace != null) + { + Accept(remainingWhitespace); + } + var remainingWhitespaceLength = Span.GetContent().Value.Length; + + // Consume any other whitespace tokens. + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); + + var hasTypeError = !At(CSharpSymbolType.Identifier); + if (hasTypeError) + { + Context.OnError( + startLocation, + Resources.FormatMvcRazorCodeParser_KeywordMustBeFollowedByTypeName(InjectKeyword), + InjectKeyword.Length); + } + + // Accept 'MyApp.MyService' + NamespaceOrTypeName(); + + // typeName now contains the token 'MyApp.MyService' + var typeName = Span.GetContent().Value; + + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); + + if (!hasTypeError && (EndOfFile || At(CSharpSymbolType.NewLine))) + { + // Add an error for the property name only if we successfully read the type name + Context.OnError( + startLocation, + Resources.FormatMvcRazorCodeParser_InjectDirectivePropertyNameRequired(InjectKeyword), + keywordwithSingleWhitespaceLength + remainingWhitespaceLength + typeName.Length); + } + + // Read until end of line. Span now contains 'MyApp.MyService MyServiceName'. + AcceptUntil(CSharpSymbolType.NewLine); + if (!Context.DesignTimeMode) + { + // We want the newline to be treated as code, but it causes issues at design-time. + Optional(CSharpSymbolType.NewLine); + } + + // Parse out 'MyServicePropertyName' from the Span. + var propertyName = Span.GetContent() + .Value + .Substring(typeName.Length); + + // ';' is optional + propertyName = RemoveWhitespaceAndTrailingSemicolons(propertyName); + Span.ChunkGenerator = new InjectParameterGenerator(typeName.Trim(), propertyName); + + // Output the span and finish the block + CompleteBlock(); + Output(SpanKind.Code, AcceptedCharacters.AnyExceptNewline); + } + + private SpanChunkGenerator CreateModelChunkGenerator(string model) + { + return new ModelChunkGenerator(model); + } + + // Internal for unit testing + internal static string RemoveWhitespaceAndTrailingSemicolons(string value) + { + Debug.Assert(value != null); + value = value.TrimStart(); + + for (var index = value.Length - 1; index >= 0; index--) + { + var currentChar = value[index]; + if (!char.IsWhiteSpace(currentChar) && currentChar != ';') + { + return value.Substring(0, index + 1); + } + } + + return string.Empty; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcRazorHost.cs b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcRazorHost.cs new file mode 100644 index 000000000..e4b4af6e2 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcRazorHost.cs @@ -0,0 +1,335 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.AspNet.FileProviders.VSRC1; +using Microsoft.AspNet.Mvc.Razor.Directives; +using Microsoft.AspNet.Mvc.Razor.Internal; +using Microsoft.AspNet.Razor; +using Microsoft.AspNet.Razor.Chunks; +using Microsoft.AspNet.Razor.CodeGenerators; +using Microsoft.AspNet.Razor.Compilation.TagHelpers; +using Microsoft.AspNet.Razor.Parser; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; +using Microsoft.AspNet.Razor.TagHelpers; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public class MvcRazorHost : RazorEngineHost, IMvcRazorHost + { + private const string BaseType = "Microsoft.AspNet.Mvc.Razor.RazorPage"; + private const string HtmlHelperPropertyName = "Html"; + + private static readonly string[] _defaultNamespaces = new[] + { + "System", + "System.Linq", + "System.Collections.Generic", + "Microsoft.AspNet.Mvc", + "Microsoft.AspNet.Mvc.Rendering", + }; + private static readonly Chunk[] _defaultInheritedChunks = new Chunk[] + { + new InjectChunk("Microsoft.AspNet.Mvc.Rendering.IHtmlHelper", HtmlHelperPropertyName), + new InjectChunk("Microsoft.AspNet.Mvc.Rendering.IJsonHelper", "Json"), + new InjectChunk("Microsoft.AspNet.Mvc.IViewComponentHelper", "Component"), + new InjectChunk("Microsoft.AspNet.Mvc.IUrlHelper", "Url"), + new AddTagHelperChunk + { + LookupText = "Microsoft.AspNet.Mvc.Razor.TagHelpers.UrlResolutionTagHelper, Microsoft.AspNet.Mvc.Razor" + }, + new SetBaseTypeChunk + { + // Microsoft.Aspnet.Mvc.Razor.RazorPage + TypeName = $"{BaseType}<{ChunkHelper.TModelToken}>", + // Set the Start to Undefined to prevent Razor design time code generation from rendering a line mapping + // for this chunk. + Start = SourceLocation.Undefined + } + }; + + // CodeGenerationContext.DefaultBaseClass is set to MyBaseType. + private readonly IChunkTreeCache _chunkTreeCache; + private readonly RazorPathNormalizer _pathNormalizer; + private ChunkInheritanceUtility _chunkInheritanceUtility; + private ITagHelperDescriptorResolver _tagHelperDescriptorResolver; + + internal MvcRazorHost(IChunkTreeCache chunkTreeCache, RazorPathNormalizer pathNormalizer) + : base(new CSharpRazorCodeLanguage()) + { + _pathNormalizer = pathNormalizer; + _chunkTreeCache = chunkTreeCache; + + DefaultBaseClass = $"{BaseType}<{ChunkHelper.TModelToken}>"; + DefaultNamespace = "Asp"; + // Enable instrumentation by default to allow precompiled views to work with BrowserLink. + EnableInstrumentation = true; + GeneratedClassContext = new GeneratedClassContext( + executeMethodName: "ExecuteAsync", + writeMethodName: "Write", + writeLiteralMethodName: "WriteLiteral", + writeToMethodName: "WriteTo", + writeLiteralToMethodName: "WriteLiteralTo", + templateTypeName: "Microsoft.AspNet.Mvc.Razor.HelperResult", + defineSectionMethodName: "DefineSection", + generatedTagHelperContext: new GeneratedTagHelperContext + { + ExecutionContextTypeName = typeof(TagHelperExecutionContext).FullName, + ExecutionContextAddMethodName = nameof(TagHelperExecutionContext.Add), + ExecutionContextAddTagHelperAttributeMethodName = + nameof(TagHelperExecutionContext.AddTagHelperAttribute), + ExecutionContextAddHtmlAttributeMethodName = nameof(TagHelperExecutionContext.AddHtmlAttribute), + ExecutionContextAddMinimizedHtmlAttributeMethodName = + nameof(TagHelperExecutionContext.AddMinimizedHtmlAttribute), + ExecutionContextOutputPropertyName = nameof(TagHelperExecutionContext.Output), + + RunnerTypeName = typeof(TagHelperRunner).FullName, + RunnerRunAsyncMethodName = nameof(TagHelperRunner.RunAsync), + + ScopeManagerTypeName = typeof(TagHelperScopeManager).FullName, + ScopeManagerBeginMethodName = nameof(TagHelperScopeManager.Begin), + ScopeManagerEndMethodName = nameof(TagHelperScopeManager.End), + + TagHelperContentTypeName = nameof(TagHelperContent), + + // Can't use nameof because RazorPage is not accessible here. + CreateTagHelperMethodName = "CreateTagHelper", + FormatInvalidIndexerAssignmentMethodName = "InvalidTagHelperIndexerAssignment", + StartTagHelperWritingScopeMethodName = "StartTagHelperWritingScope", + EndTagHelperWritingScopeMethodName = "EndTagHelperWritingScope", + + WriteTagHelperAsyncMethodName = "WriteTagHelperAsync", + WriteTagHelperToAsyncMethodName = "WriteTagHelperToAsync", + + // Can't use nameof because IHtmlHelper is (also) not accessible here. + MarkAsHtmlEncodedMethodName = HtmlHelperPropertyName + ".Raw", + BeginAddHtmlAttributeValuesMethodName = "BeginAddHtmlAttributeValues", + EndAddHtmlAttributeValuesMethodName = "EndAddHtmlAttributeValues", + AddHtmlAttributeValueMethodName = "AddHtmlAttributeValue", + HtmlEncoderPropertyName = "HtmlEncoder", + TagHelperContentGetContentMethodName = nameof(TagHelperContent.GetContent), + }) + { + BeginContextMethodName = "BeginContext", + EndContextMethodName = "EndContext" + }; + + foreach (var ns in _defaultNamespaces) + { + NamespaceImports.Add(ns); + } + } + + /// + /// Initializes a new instance of with the specified . + /// + /// The path to the application base. + // Note: This constructor is used by tooling and is created once for each + // Razor page that is loaded. Consequently, each loaded page has its own copy of + // the ChunkTreeCache, but this ok - having a shared ChunkTreeCache per application in tooling + // is problematic to manage. + public MvcRazorHost(string root) + : this(new DefaultChunkTreeCache(new PhysicalFileProvider(root)), new DesignTimeRazorPathNormalizer(root)) + { + } + + /// + /// Initializes a new instance of using the specified . + /// + /// An rooted at the application base path. + public MvcRazorHost(IChunkTreeCache chunkTreeCache) + : this(chunkTreeCache, new RazorPathNormalizer()) + { + } + + /// + public override ITagHelperDescriptorResolver TagHelperDescriptorResolver + { + get + { + // The initialization of the _tagHelperDescriptorResolver needs to be lazy to allow for the setting + // of DesignTimeMode. + if (_tagHelperDescriptorResolver == null) + { + _tagHelperDescriptorResolver = new TagHelperDescriptorResolver(DesignTimeMode); + } + + return _tagHelperDescriptorResolver; + } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _tagHelperDescriptorResolver = value; + } + } + + /// + /// Gets the model type used by default when no model is specified. + /// + /// This value is used as the generic type argument for the base type + public virtual string DefaultModel + { + get { return "dynamic"; } + } + + /// + public string MainClassNamePrefix + { + get { return "ASPV_"; } + } + + /// + /// Gets the list of chunks that are injected by default by this host. + /// + public virtual IReadOnlyList DefaultInheritedChunks + { + get { return _defaultInheritedChunks; } + } + + /// + /// Gets or sets the name attribute that is used to decorate properties that are injected and need to be + /// activated. + /// + public virtual string InjectAttribute + { + get { return "Microsoft.AspNet.Mvc.Razor.Internal.RazorInjectAttribute"; } + } + + /// + /// Gets the type name used to represent model expression properties. + /// + public virtual string ModelExpressionType + { + get { return "Microsoft.AspNet.Mvc.Rendering.ModelExpression"; } + } + + /// + /// Gets the method name used to create model expressions. + /// + public virtual string CreateModelExpressionMethod + { + get { return "CreateModelExpression"; } + } + + // Internal for testing + internal ChunkInheritanceUtility ChunkInheritanceUtility + { + get + { + if (_chunkInheritanceUtility == null) + { + // This needs to be lazily evaluated to support DefaultInheritedChunks being virtual. + _chunkInheritanceUtility = new ChunkInheritanceUtility(this, _chunkTreeCache, DefaultInheritedChunks); + } + + return _chunkInheritanceUtility; + } + set + { + _chunkInheritanceUtility = value; + } + } + + /// + /// Locates and parses _ViewImports.cshtml files applying to the given to + /// create s. + /// + /// The path to a Razor file to locate _ViewImports.cshtml for. + /// Inherited s. + public IReadOnlyList GetInheritedChunkTreeResults(string sourceFileName) + { + if (sourceFileName == null) + { + throw new ArgumentNullException(nameof(sourceFileName)); + } + + // Need the normalized path to resolve inherited chunks only. Full paths are needed for generated Razor + // files checksum and line pragmas to enable DesignTime debugging. + var normalizedPath = _pathNormalizer.NormalizePath(sourceFileName); + + return ChunkInheritanceUtility.GetInheritedChunkTreeResults(normalizedPath); + } + + /// + public GeneratorResults GenerateCode(string rootRelativePath, Stream inputStream) + { + // Adding a prefix so that the main view class can be easily identified. + var className = MainClassNamePrefix + ParserHelpers.SanitizeClassName(rootRelativePath); + var engine = new RazorTemplateEngine(this); + return engine.GenerateCode(inputStream, className, DefaultNamespace, rootRelativePath); + } + + /// + public override RazorParser DecorateRazorParser(RazorParser razorParser, string sourceFileName) + { + if (razorParser == null) + { + throw new ArgumentNullException(nameof(razorParser)); + } + + var inheritedChunkTrees = GetInheritedChunkTrees(sourceFileName); + + return new MvcRazorParser(razorParser, inheritedChunkTrees, DefaultInheritedChunks, ModelExpressionType); + } + + /// + public override ParserBase DecorateCodeParser(ParserBase incomingCodeParser) + { + if (incomingCodeParser == null) + { + throw new ArgumentNullException(nameof(incomingCodeParser)); + } + + return new MvcRazorCodeParser(); + } + + /// + public override CodeGenerator DecorateCodeGenerator( + CodeGenerator incomingGenerator, + CodeGeneratorContext context) + { + if (incomingGenerator == null) + { + throw new ArgumentNullException(nameof(incomingGenerator)); + } + + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var inheritedChunkTrees = GetInheritedChunkTrees(context.SourceFile); + + ChunkInheritanceUtility.MergeInheritedChunkTrees( + context.ChunkTreeBuilder.ChunkTree, + inheritedChunkTrees, + DefaultModel); + + return new MvcCSharpCodeGenerator( + context, + DefaultModel, + InjectAttribute, + new GeneratedTagHelperAttributeContext + { + ModelExpressionTypeName = ModelExpressionType, + CreateModelExpressionMethodName = CreateModelExpressionMethod + }); + } + + private IReadOnlyList GetInheritedChunkTrees(string sourceFileName) + { + var inheritedChunkTrees = GetInheritedChunkTreeResults(sourceFileName) + .Select(result => result.ChunkTree) + .ToList(); + + return inheritedChunkTrees; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcRazorParser.cs b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcRazorParser.cs new file mode 100644 index 000000000..0e8ac8f86 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcRazorParser.cs @@ -0,0 +1,229 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.Mvc.Razor.Host; +using Microsoft.AspNet.Razor; +using Microsoft.AspNet.Razor.Chunks; +using Microsoft.AspNet.Razor.Compilation.TagHelpers; +using Microsoft.AspNet.Razor.Parser; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Parser.TagHelpers; + +namespace Microsoft.AspNet.Mvc.Razor +{ + /// + /// A subtype of that uses to support inheritance of tag + /// helpers from _ViewImports files. + /// + public class MvcRazorParser : RazorParser + { + private readonly IEnumerable _viewImportsDirectiveDescriptors; + private readonly string _modelExpressionTypeName; + + /// + /// Initializes a new instance of . + /// + /// The to copy properties from. + /// The s that are inherited + /// from parsed pages from _ViewImports files. + /// The inherited by + /// default by all Razor pages in the application. + public MvcRazorParser( + RazorParser parser, + IReadOnlyList inheritedChunkTrees, + IReadOnlyList defaultInheritedChunks, + string modelExpressionTypeName) + : base(parser) + { + if (parser == null) + { + throw new ArgumentNullException(nameof(parser)); + } + + if (inheritedChunkTrees == null) + { + throw new ArgumentNullException(nameof(inheritedChunkTrees)); + } + + if (defaultInheritedChunks == null) + { + throw new ArgumentNullException(nameof(defaultInheritedChunks)); + } + + if (modelExpressionTypeName == null) + { + throw new ArgumentNullException(nameof(modelExpressionTypeName)); + } + + // Construct tag helper descriptors from @addTagHelper, @removeTagHelper and @tagHelperPrefix chunks + _viewImportsDirectiveDescriptors = GetTagHelperDirectiveDescriptors( + inheritedChunkTrees, + defaultInheritedChunks); + + _modelExpressionTypeName = modelExpressionTypeName; + } + + /// + protected override IEnumerable GetTagHelperDescriptors( + Block documentRoot, + ErrorSink errorSink) + { + if (documentRoot == null) + { + throw new ArgumentNullException(nameof(documentRoot)); + } + + if (errorSink == null) + { + throw new ArgumentNullException(nameof(errorSink)); + } + + var visitor = new ViewImportsTagHelperDirectiveSpanVisitor( + TagHelperDescriptorResolver, + _viewImportsDirectiveDescriptors, + errorSink); + + var descriptors = visitor.GetDescriptors(documentRoot); + foreach (var descriptor in descriptors) + { + foreach (var attributeDescriptor in descriptor.Attributes) + { + if (attributeDescriptor.IsIndexer && + string.Equals( + attributeDescriptor.TypeName, + _modelExpressionTypeName, + StringComparison.Ordinal)) + { + errorSink.OnError( + SourceLocation.Undefined, + Resources.FormatMvcRazorParser_InvalidPropertyType( + descriptor.TypeName, + attributeDescriptor.Name, + _modelExpressionTypeName), + length: 0); + } + } + } + + return descriptors; + } + + private static IEnumerable GetTagHelperDirectiveDescriptors( + IReadOnlyList inheritedChunkTrees, + IReadOnlyList defaultInheritedChunks) + { + var descriptors = new List(); + + var inheritedChunks = defaultInheritedChunks.Concat(inheritedChunkTrees.SelectMany(tree => tree.Chunks)); + foreach (var chunk in inheritedChunks) + { + // All TagHelperDirectiveDescriptors created here have undefined source locations because the source + // that created them is not in the same file. + var addTagHelperChunk = chunk as AddTagHelperChunk; + if (addTagHelperChunk != null) + { + var descriptor = new TagHelperDirectiveDescriptor + { + DirectiveText = addTagHelperChunk.LookupText, + Location = chunk.Start, + DirectiveType = TagHelperDirectiveType.AddTagHelper + }; + + descriptors.Add(descriptor); + + continue; + } + + var removeTagHelperChunk = chunk as RemoveTagHelperChunk; + if (removeTagHelperChunk != null) + { + var descriptor = new TagHelperDirectiveDescriptor + { + DirectiveText = removeTagHelperChunk.LookupText, + Location = chunk.Start, + DirectiveType = TagHelperDirectiveType.RemoveTagHelper + }; + + descriptors.Add(descriptor); + + continue; + } + + var tagHelperPrefixDirectiveChunk = chunk as TagHelperPrefixDirectiveChunk; + if (tagHelperPrefixDirectiveChunk != null) + { + var descriptor = new TagHelperDirectiveDescriptor + { + DirectiveText = tagHelperPrefixDirectiveChunk.Prefix, + Location = chunk.Start, + DirectiveType = TagHelperDirectiveType.TagHelperPrefix + }; + + descriptors.Add(descriptor); + } + } + + return descriptors; + } + + private class ViewImportsTagHelperDirectiveSpanVisitor : TagHelperDirectiveSpanVisitor + { + private readonly IEnumerable _viewImportsDirectiveDescriptors; + + public ViewImportsTagHelperDirectiveSpanVisitor( + ITagHelperDescriptorResolver descriptorResolver, + IEnumerable viewImportsDirectiveDescriptors, + ErrorSink errorSink) + : base(descriptorResolver, errorSink) + { + _viewImportsDirectiveDescriptors = viewImportsDirectiveDescriptors; + } + + protected override TagHelperDescriptorResolutionContext GetTagHelperDescriptorResolutionContext( + IEnumerable descriptors, + ErrorSink errorSink) + { + var directivesToImport = MergeDirectiveDescriptors(descriptors, _viewImportsDirectiveDescriptors); + + return base.GetTagHelperDescriptorResolutionContext(directivesToImport, errorSink); + } + + private static IEnumerable MergeDirectiveDescriptors( + IEnumerable descriptors, + IEnumerable inheritedDescriptors) + { + var mergedDescriptors = new List(); + TagHelperDirectiveDescriptor prefixDirectiveDescriptor = null; + + foreach (var descriptor in inheritedDescriptors) + { + if (descriptor.DirectiveType == TagHelperDirectiveType.TagHelperPrefix) + { + // Always take the latest @tagHelperPrefix descriptor. Can only have 1 per page. + prefixDirectiveDescriptor = descriptor; + } + else + { + mergedDescriptors.Add(descriptor); + } + } + + // We need to see if the provided descriptors contain a @tagHelperPrefix directive. If so, it + // takes precedence and overrides any provided by the inheritedDescriptors. If not we need to add the + // inherited @tagHelperPrefix directive back into the merged list. + if (prefixDirectiveDescriptor != null && + !descriptors.Any(descriptor => descriptor.DirectiveType == TagHelperDirectiveType.TagHelperPrefix)) + { + mergedDescriptors.Add(prefixDirectiveDescriptor); + } + + mergedDescriptors.AddRange(descriptors); + + return mergedDescriptors; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcTagHelperAttributeValueCodeRenderer.cs b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcTagHelperAttributeValueCodeRenderer.cs new file mode 100644 index 000000000..aa467213e --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/MvcTagHelperAttributeValueCodeRenderer.cs @@ -0,0 +1,92 @@ +// 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.AspNet.Razor.CodeGenerators; +using Microsoft.AspNet.Razor.Compilation.TagHelpers; + +namespace Microsoft.AspNet.Mvc.Razor +{ + /// + public class MvcTagHelperAttributeValueCodeRenderer : TagHelperAttributeValueCodeRenderer + { + private const string ModelLambdaVariableName = "__model"; + + private readonly GeneratedTagHelperAttributeContext _context; + + /// + /// Instantiates a new instance of . + /// + /// Contains code generation information for rendering attribute values. + public MvcTagHelperAttributeValueCodeRenderer(GeneratedTagHelperAttributeContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + _context = context; + } + + /// + /// If the attribute being rendered is of the type + /// , then a model expression will be + /// created by calling into . + /// + public override void RenderAttributeValue( + TagHelperAttributeDescriptor attributeDescriptor, + CSharpCodeWriter writer, + CodeGeneratorContext codeGeneratorContext, + Action renderAttributeValue, + bool complexValue) + { + if (attributeDescriptor == null) + { + throw new ArgumentNullException(nameof(attributeDescriptor)); + } + + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (codeGeneratorContext == null) + { + throw new ArgumentNullException(nameof(codeGeneratorContext)); + } + + if (renderAttributeValue == null) + { + throw new ArgumentNullException(nameof(renderAttributeValue)); + } + + if (attributeDescriptor.TypeName.Equals(_context.ModelExpressionTypeName, StringComparison.Ordinal)) + { + writer + .WriteStartMethodInvocation(_context.CreateModelExpressionMethodName) + .Write(ModelLambdaVariableName) + .Write(" => "); + if (!complexValue) + { + writer + .Write(ModelLambdaVariableName) + .Write("."); + + } + + renderAttributeValue(writer); + + writer.WriteEndMethodInvocation(endLine: false); + } + else + { + base.RenderAttributeValue( + attributeDescriptor, + writer, + codeGeneratorContext, + renderAttributeValue, + complexValue); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..1bbf723e7 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Properties/AssemblyInfo.cs @@ -0,0 +1,9 @@ +// 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.Reflection; +using System.Resources; +using System.Runtime.CompilerServices; + +[assembly: AssemblyMetadata("Serviceable", "True")] +[assembly: NeutralResourcesLanguage("en-us")] \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Properties/Resources.Designer.cs new file mode 100644 index 000000000..954652337 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Properties/Resources.Designer.cs @@ -0,0 +1,126 @@ +// +namespace Microsoft.AspNet.Mvc.Razor.Host +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNet.Mvc.Razor.Host.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// Value cannot be null or empty. + /// + internal static string ArgumentCannotBeNullOrEmpy + { + get { return GetString("ArgumentCannotBeNullOrEmpy"); } + } + + /// + /// Value cannot be null or empty. + /// + internal static string FormatArgumentCannotBeNullOrEmpy() + { + return GetString("ArgumentCannotBeNullOrEmpy"); + } + + /// + /// The 'inherits' keyword is not allowed when a '{0}' keyword is used. + /// + internal static string MvcRazorCodeParser_CannotHaveModelAndInheritsKeyword + { + get { return GetString("MvcRazorCodeParser_CannotHaveModelAndInheritsKeyword"); } + } + + /// + /// The 'inherits' keyword is not allowed when a '{0}' keyword is used. + /// + internal static string FormatMvcRazorCodeParser_CannotHaveModelAndInheritsKeyword(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("MvcRazorCodeParser_CannotHaveModelAndInheritsKeyword"), p0); + } + + /// + /// A property name must be specified when using the '{0}' statement. Format for a '{0}' statement is '@{0} <Type Name> <Property Name>'. + /// + internal static string MvcRazorCodeParser_InjectDirectivePropertyNameRequired + { + get { return GetString("MvcRazorCodeParser_InjectDirectivePropertyNameRequired"); } + } + + /// + /// A property name must be specified when using the '{0}' statement. Format for a '{0}' statement is '@{0} <Type Name> <Property Name>'. + /// + internal static string FormatMvcRazorCodeParser_InjectDirectivePropertyNameRequired(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("MvcRazorCodeParser_InjectDirectivePropertyNameRequired"), p0); + } + + /// + /// The '{0}' keyword must be followed by a type name on the same line. + /// + internal static string MvcRazorCodeParser_KeywordMustBeFollowedByTypeName + { + get { return GetString("MvcRazorCodeParser_KeywordMustBeFollowedByTypeName"); } + } + + /// + /// The '{0}' keyword must be followed by a type name on the same line. + /// + internal static string FormatMvcRazorCodeParser_KeywordMustBeFollowedByTypeName(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("MvcRazorCodeParser_KeywordMustBeFollowedByTypeName"), p0); + } + + /// + /// Only one '{0}' statement is allowed in a file. + /// + internal static string MvcRazorCodeParser_OnlyOneModelStatementIsAllowed + { + get { return GetString("MvcRazorCodeParser_OnlyOneModelStatementIsAllowed"); } + } + + /// + /// Only one '{0}' statement is allowed in a file. + /// + internal static string FormatMvcRazorCodeParser_OnlyOneModelStatementIsAllowed(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("MvcRazorCodeParser_OnlyOneModelStatementIsAllowed"), p0); + } + + /// + /// Invalid tag helper property '{0}.{1}'. Dictionary values must not be of type '{2}'. + /// + internal static string MvcRazorParser_InvalidPropertyType + { + get { return GetString("MvcRazorParser_InvalidPropertyType"); } + } + + /// + /// Invalid tag helper property '{0}.{1}'. Dictionary values must not be of type '{2}'. + /// + internal static string FormatMvcRazorParser_InvalidPropertyType(object p0, object p1, object p2) + { + return string.Format(CultureInfo.CurrentCulture, GetString("MvcRazorParser_InvalidPropertyType"), p0, p1, p2); + } + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/PropertyHelper.cs b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/PropertyHelper.cs new file mode 100644 index 000000000..3330e4144 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/PropertyHelper.cs @@ -0,0 +1,513 @@ +// 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.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; + +namespace Microsoft.Extensions.Internal +{ + internal class PropertyHelper + { + // Delegate type for a by-ref property getter + private delegate TValue ByRefFunc(ref TDeclaringType arg); + + private static readonly MethodInfo CallPropertyGetterOpenGenericMethod = + typeof(PropertyHelper).GetTypeInfo().GetDeclaredMethod(nameof(CallPropertyGetter)); + + private static readonly MethodInfo CallPropertyGetterByReferenceOpenGenericMethod = + typeof(PropertyHelper).GetTypeInfo().GetDeclaredMethod(nameof(CallPropertyGetterByReference)); + + private static readonly MethodInfo CallNullSafePropertyGetterOpenGenericMethod = + typeof(PropertyHelper).GetTypeInfo().GetDeclaredMethod(nameof(CallNullSafePropertyGetter)); + + private static readonly MethodInfo CallNullSafePropertyGetterByReferenceOpenGenericMethod = + typeof(PropertyHelper).GetTypeInfo().GetDeclaredMethod(nameof(CallNullSafePropertyGetterByReference)); + + private static readonly MethodInfo CallPropertySetterOpenGenericMethod = + typeof(PropertyHelper).GetTypeInfo().GetDeclaredMethod(nameof(CallPropertySetter)); + + // Using an array rather than IEnumerable, as target will be called on the hot path numerous times. + private static readonly ConcurrentDictionary PropertiesCache = + new ConcurrentDictionary(); + + private static readonly ConcurrentDictionary VisiblePropertiesCache = + new ConcurrentDictionary(); + + private Action _valueSetter; + + /// + /// Initializes a fast . + /// This constructor does not cache the helper. For caching, use . + /// + public PropertyHelper(PropertyInfo property) + { + if (property == null) + { + throw new ArgumentNullException(nameof(property)); + } + + Property = property; + Name = property.Name; + ValueGetter = MakeFastPropertyGetter(property); + } + + /// + /// Gets the backing . + /// + public PropertyInfo Property { get; } + + /// + /// Gets (or sets in derived types) the property name. + /// + public virtual string Name { get; protected set; } + + /// + /// Gets the property value getter. + /// + public Func ValueGetter { get; } + + /// + /// Gets the property value setter. + /// + public Action ValueSetter + { + get + { + if (_valueSetter == null) + { + // We'll allow safe races here. + _valueSetter = MakeFastPropertySetter(Property); + } + + return _valueSetter; + } + } + + /// + /// Returns the property value for the specified . + /// + /// The object whose property value will be returned. + /// The property value. + public object GetValue(object instance) + { + return ValueGetter(instance); + } + + /// + /// Sets the property value for the specified . + /// + /// The object whose property value will be set. + /// The property value. + public void SetValue(object instance, object value) + { + ValueSetter(instance, value); + } + + /// + /// Creates and caches fast property helpers that expose getters for every public get property on the + /// underlying type. + /// + /// the instance to extract property accessors for. + /// a cached array of all public property getters from the underlying type of target instance. + /// + public static PropertyHelper[] GetProperties(object instance) + { + return GetProperties(instance.GetType()); + } + + /// + /// Creates and caches fast property helpers that expose getters for every public get property on the + /// specified type. + /// + /// the type to extract property accessors for. + /// a cached array of all public property getters from the type of target instance. + /// + public static PropertyHelper[] GetProperties(Type type) + { + return GetProperties(type, CreateInstance, PropertiesCache); + } + + /// + /// + /// Creates and caches fast property helpers that expose getters for every non-hidden get property + /// on the specified type. + /// + /// + /// excludes properties defined on base types that have been + /// hidden by definitions using the new keyword. + /// + /// + /// The instance to extract property accessors for. + /// + /// A cached array of all public property getters from the instance's type. + /// + public static PropertyHelper[] GetVisibleProperties(object instance) + { + return GetVisibleProperties(instance.GetType(), CreateInstance, PropertiesCache, VisiblePropertiesCache); + } + + /// + /// + /// Creates a caches fast property helpers that expose getters for every non-hidden get property + /// on the specified type. + /// + /// + /// excludes properties defined on base types that have been + /// hidden by definitions using the new keyword. + /// + /// + /// The type to extract property accessors for. + /// + /// A cached array of all public property getters from the type. + /// + public static PropertyHelper[] GetVisibleProperties(Type type) + { + return GetVisibleProperties(type, CreateInstance, PropertiesCache, VisiblePropertiesCache); + } + + /// + /// Creates a single fast property getter. The result is not cached. + /// + /// propertyInfo to extract the getter for. + /// a fast getter. + /// + /// This method is more memory efficient than a dynamically compiled lambda, and about the + /// same speed. + /// + public static Func MakeFastPropertyGetter(PropertyInfo propertyInfo) + { + Debug.Assert(propertyInfo != null); + + return MakeFastPropertyGetter( + propertyInfo, + CallPropertyGetterOpenGenericMethod, + CallPropertyGetterByReferenceOpenGenericMethod); + } + + /// + /// Creates a single fast property getter which is safe for a null input object. The result is not cached. + /// + /// propertyInfo to extract the getter for. + /// a fast getter. + /// + /// This method is more memory efficient than a dynamically compiled lambda, and about the + /// same speed. + /// + public static Func MakeNullSafeFastPropertyGetter(PropertyInfo propertyInfo) + { + Debug.Assert(propertyInfo != null); + + return MakeFastPropertyGetter( + propertyInfo, + CallNullSafePropertyGetterOpenGenericMethod, + CallNullSafePropertyGetterByReferenceOpenGenericMethod); + } + + private static Func MakeFastPropertyGetter( + PropertyInfo propertyInfo, + MethodInfo propertyGetterWrapperMethod, + MethodInfo propertyGetterByRefWrapperMethod) + { + Debug.Assert(propertyInfo != null); + + // Must be a generic method with a Func<,> parameter + Debug.Assert(propertyGetterWrapperMethod != null); + Debug.Assert(propertyGetterWrapperMethod.IsGenericMethodDefinition); + Debug.Assert(propertyGetterWrapperMethod.GetParameters().Length == 2); + + // Must be a generic method with a ByRefFunc<,> parameter + Debug.Assert(propertyGetterByRefWrapperMethod != null); + Debug.Assert(propertyGetterByRefWrapperMethod.IsGenericMethodDefinition); + Debug.Assert(propertyGetterByRefWrapperMethod.GetParameters().Length == 2); + + var getMethod = propertyInfo.GetMethod; + Debug.Assert(getMethod != null); + Debug.Assert(!getMethod.IsStatic); + Debug.Assert(getMethod.GetParameters().Length == 0); + + // Instance methods in the CLR can be turned into static methods where the first parameter + // is open over "target". This parameter is always passed by reference, so we have a code + // path for value types and a code path for reference types. + if (getMethod.DeclaringType.GetTypeInfo().IsValueType) + { + // Create a delegate (ref TDeclaringType) -> TValue + return MakeFastPropertyGetter( + typeof(ByRefFunc<,>), + getMethod, + propertyGetterByRefWrapperMethod); + } + else + { + // Create a delegate TDeclaringType -> TValue + return MakeFastPropertyGetter( + typeof(Func<,>), + getMethod, + propertyGetterWrapperMethod); + } + } + + private static Func MakeFastPropertyGetter( + Type openGenericDelegateType, + MethodInfo propertyGetMethod, + MethodInfo openGenericWrapperMethod) + { + var typeInput = propertyGetMethod.DeclaringType; + var typeOutput = propertyGetMethod.ReturnType; + + var delegateType = openGenericDelegateType.MakeGenericType(typeInput, typeOutput); + var propertyGetterDelegate = propertyGetMethod.CreateDelegate(delegateType); + + var wrapperDelegateMethod = openGenericWrapperMethod.MakeGenericMethod(typeInput, typeOutput); + var accessorDelegate = wrapperDelegateMethod.CreateDelegate( + typeof(Func), + propertyGetterDelegate); + + return (Func)accessorDelegate; + } + + /// + /// Creates a single fast property setter for reference types. The result is not cached. + /// + /// propertyInfo to extract the setter for. + /// a fast getter. + /// + /// This method is more memory efficient than a dynamically compiled lambda, and about the + /// same speed. This only works for reference types. + /// + public static Action MakeFastPropertySetter(PropertyInfo propertyInfo) + { + Debug.Assert(propertyInfo != null); + Debug.Assert(!propertyInfo.DeclaringType.GetTypeInfo().IsValueType); + + var setMethod = propertyInfo.SetMethod; + Debug.Assert(setMethod != null); + Debug.Assert(!setMethod.IsStatic); + Debug.Assert(setMethod.ReturnType == typeof(void)); + var parameters = setMethod.GetParameters(); + Debug.Assert(parameters.Length == 1); + + // Instance methods in the CLR can be turned into static methods where the first parameter + // is open over "target". This parameter is always passed by reference, so we have a code + // path for value types and a code path for reference types. + var typeInput = setMethod.DeclaringType; + var parameterType = parameters[0].ParameterType; + + // Create a delegate TDeclaringType -> { TDeclaringType.Property = TValue; } + var propertySetterAsAction = + setMethod.CreateDelegate(typeof(Action<,>).MakeGenericType(typeInput, parameterType)); + var callPropertySetterClosedGenericMethod = + CallPropertySetterOpenGenericMethod.MakeGenericMethod(typeInput, parameterType); + var callPropertySetterDelegate = + callPropertySetterClosedGenericMethod.CreateDelegate( + typeof(Action), propertySetterAsAction); + + return (Action)callPropertySetterDelegate; + } + + /// + /// Given an object, adds each instance property with a public get method as a key and its + /// associated value to a dictionary. + /// + /// If the object is already an instance, then a copy + /// is returned. + /// + /// + /// The implementation of PropertyHelper will cache the property accessors per-type. This is + /// faster when the the same type is used multiple times with ObjectToDictionary. + /// + public static IDictionary ObjectToDictionary(object value) + { + var dictionary = value as IDictionary; + if (dictionary != null) + { + return new Dictionary(dictionary, StringComparer.OrdinalIgnoreCase); + } + + dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (value != null) + { + foreach (var helper in GetProperties(value)) + { + dictionary[helper.Name] = helper.GetValue(value); + } + } + + return dictionary; + } + + private static PropertyHelper CreateInstance(PropertyInfo property) + { + return new PropertyHelper(property); + } + + // Called via reflection + private static object CallPropertyGetter( + Func getter, + object target) + { + return getter((TDeclaringType)target); + } + + // Called via reflection + private static object CallPropertyGetterByReference( + ByRefFunc getter, + object target) + { + var unboxed = (TDeclaringType)target; + return getter(ref unboxed); + } + + // Called via reflection + private static object CallNullSafePropertyGetter( + Func getter, + object target) + { + if (target == null) + { + return null; + } + + return getter((TDeclaringType)target); + } + + // Called via reflection + private static object CallNullSafePropertyGetterByReference( + ByRefFunc getter, + object target) + { + if (target == null) + { + return null; + } + + var unboxed = (TDeclaringType)target; + return getter(ref unboxed); + } + + private static void CallPropertySetter( + Action setter, + object target, + object value) + { + setter((TDeclaringType)target, (TValue)value); + } + + protected static PropertyHelper[] GetVisibleProperties( + Type type, + Func createPropertyHelper, + ConcurrentDictionary allPropertiesCache, + ConcurrentDictionary visiblePropertiesCache) + { + PropertyHelper[] result; + if (visiblePropertiesCache.TryGetValue(type, out result)) + { + return result; + } + + // The simple and common case, this is normal POCO object - no need to allocate. + var allPropertiesDefinedOnType = true; + var allProperties = GetProperties(type, createPropertyHelper, allPropertiesCache); + foreach (var propertyHelper in allProperties) + { + if (propertyHelper.Property.DeclaringType != type) + { + allPropertiesDefinedOnType = false; + break; + } + } + + if (allPropertiesDefinedOnType) + { + result = allProperties; + visiblePropertiesCache.TryAdd(type, result); + return result; + } + + // There's some inherited properties here, so we need to check for hiding via 'new'. + var filteredProperties = new List(allProperties.Length); + foreach (var propertyHelper in allProperties) + { + var declaringType = propertyHelper.Property.DeclaringType; + if (declaringType == type) + { + filteredProperties.Add(propertyHelper); + continue; + } + + // If this property was declared on a base type then look for the definition closest to the + // the type to see if we should include it. + var ignoreProperty = false; + + // Walk up the hierarchy until we find the type that actally declares this + // PropertyInfo. + var currentTypeInfo = type.GetTypeInfo(); + var declaringTypeInfo = declaringType.GetTypeInfo(); + while (currentTypeInfo != null && currentTypeInfo != declaringTypeInfo) + { + // We've found a 'more proximal' public definition + var declaredProperty = currentTypeInfo.GetDeclaredProperty(propertyHelper.Name); + if (declaredProperty != null) + { + ignoreProperty = true; + break; + } + + currentTypeInfo = currentTypeInfo.BaseType?.GetTypeInfo(); + } + + if (!ignoreProperty) + { + filteredProperties.Add(propertyHelper); + } + } + + result = filteredProperties.ToArray(); + visiblePropertiesCache.TryAdd(type, result); + return result; + } + + protected static PropertyHelper[] GetProperties( + Type type, + Func createPropertyHelper, + ConcurrentDictionary cache) + { + // Unwrap nullable types. This means Nullable.Value and Nullable.HasValue will not be + // part of the sequence of properties returned by this method. + type = Nullable.GetUnderlyingType(type) ?? type; + + PropertyHelper[] helpers; + if (!cache.TryGetValue(type, out helpers)) + { + // We avoid loading indexed properties using the Where statement. + var properties = type.GetRuntimeProperties().Where(IsInterestingProperty); + + var typeInfo = type.GetTypeInfo(); + if (typeInfo.IsInterface) + { + // Reflection does not return information about inherited properties on the interface itself. + properties = properties.Concat(typeInfo.ImplementedInterfaces.SelectMany( + interfaceType => interfaceType.GetRuntimeProperties().Where(IsInterestingProperty))); + } + + helpers = properties.Select(p => createPropertyHelper(p)).ToArray(); + cache.TryAdd(type, helpers); + } + + return helpers; + } + + // Indexed properties are not useful (or valid) for grabbing properties off an object. + private static bool IsInterestingProperty(PropertyInfo property) + { + return property.GetIndexParameters().Length == 0 && + property.GetMethod != null && + property.GetMethod.IsPublic && + !property.GetMethod.IsStatic; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Resources.resx b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Resources.resx new file mode 100644 index 000000000..a1c3afc65 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/Resources.resx @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Value cannot be null or empty. + + + The 'inherits' keyword is not allowed when a '{0}' keyword is used. + + + A property name must be specified when using the '{0}' statement. Format for a '{0}' statement is '@{0} <Type Name> <Property Name>'. + + + The '{0}' keyword must be followed by a type name on the same line. + + + Only one '{0}' statement is allowed in a file. + + + Invalid tag helper property '{0}.{1}'. Dictionary values must not be of type '{2}'. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/ViewHierarchyUtility.cs b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/ViewHierarchyUtility.cs new file mode 100644 index 000000000..2497f75ac --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/ViewHierarchyUtility.cs @@ -0,0 +1,111 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Microsoft.AspNet.Mvc.Razor +{ + /// + /// Contains methods to locate _ViewStart.cshtml and _ViewImports.cshtml + /// + public static class ViewHierarchyUtility + { + private const string ViewStartFileName = "_ViewStart.cshtml"; + + /// + /// File name of _ViewImports.cshtml file + /// + public static readonly string ViewImportsFileName = "_ViewImports.cshtml"; + + /// + /// Gets the view start locations that are applicable to the specified path. + /// + /// The application relative path of the file to locate + /// _ViewStarts for. + /// A sequence of paths that represent potential view start locations. + /// + /// This method returns paths starting from the directory of and + /// moves upwards until it hits the application root. + /// e.g. + /// /Views/Home/View.cshtml -> [ /Views/Home/_ViewStart.cshtml, /Views/_ViewStart.cshtml, /_ViewStart.cshtml ] + /// + public static IEnumerable GetViewStartLocations(string applicationRelativePath) + { + return GetHierarchicalPath(applicationRelativePath, ViewStartFileName); + } + + /// + /// Gets the locations for _ViewImportss that are applicable to the specified path. + /// + /// The application relative path of the file to locate + /// _ViewImportss for. + /// A sequence of paths that represent potential _ViewImports locations. + /// + /// This method returns paths starting from the directory of and + /// moves upwards until it hits the application root. + /// e.g. + /// /Views/Home/View.cshtml -> [ /Views/Home/_ViewImports.cshtml, /Views/_ViewImports.cshtml, + /// /_ViewImports.cshtml ] + /// + public static IEnumerable GetViewImportsLocations(string applicationRelativePath) + { + return GetHierarchicalPath(applicationRelativePath, ViewImportsFileName); + } + + private static IEnumerable GetHierarchicalPath(string relativePath, string fileName) + { + if (string.IsNullOrEmpty(relativePath)) + { + return Enumerable.Empty(); + } + + if (relativePath.StartsWith("~/", StringComparison.Ordinal)) + { + relativePath = relativePath.Substring(2); + } + + if (relativePath.StartsWith("/", StringComparison.Ordinal)) + { + relativePath = relativePath.Substring(1); + } + + if (string.Equals(Path.GetFileName(relativePath), fileName, StringComparison.OrdinalIgnoreCase)) + { + // If the specified path is for the file hierarchy being constructed, then the first file that applies + // to it is in a parent directory. + relativePath = Path.GetDirectoryName(relativePath); + + if (string.IsNullOrEmpty(relativePath)) + { + return Enumerable.Empty(); + } + } + + var builder = new StringBuilder(relativePath); + builder.Replace('\\', '/'); + + if (builder.Length > 0 && builder[0] != '/') + { + builder.Insert(0, '/'); + } + + var locations = new List(); + for (var index = builder.Length - 1; index >= 0; index--) + { + if (builder[index] == '/') + { + builder.Length = index + 1; + builder.Append(fileName); + + locations.Add(builder.ToString()); + } + } + + return locations; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/project.json b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/project.json new file mode 100644 index 000000000..8526f3f05 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host.VSRC1/project.json @@ -0,0 +1,29 @@ +{ + "description": "Design time hosting infrastructure for the ASP.NET MVC Razor view engine.", + "version": "6.0.0-rc1-final", + "repository": { + "type": "git", + "url": "git://github.com/aspnet/mvc" + }, + "compilationOptions": { + "warningsAsErrors": true, + "keyFile": "../../tools/Key.snk" + }, + "dependencies": { + "Microsoft.AspNet.FileProviders.Physical.VSRC1": "1.0.0-rc1-final", + "Microsoft.AspNet.Razor.Runtime.VSRC1": "4.0.0-rc1-final", + "Microsoft.Extensions.Caching.Memory.VSRC1": "1.0.0-rc1-final" + }, + "frameworks": { + "net451": { + "dependencies": { + "Microsoft.Extensions.PlatformAbstractions": "1.0.0-rc1-final" + } + } + }, + "exclude": [ + "wwwroot", + "node_modules", + "bower_components" + ] +} \ No newline at end of file