diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index fd85abb361..0000000000 --- a/.appveyor.yml +++ /dev/null @@ -1,17 +0,0 @@ -init: -- git config --global core.autocrlf true -branches: - only: - - dev - - /^release\/.*$/ - - /^(.*\/)?ci-.*$/ -build_script: -- ps: .\run.ps1 default-build -clone_depth: 1 -environment: - global: - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true - DOTNET_CLI_TELEMETRY_OPTOUT: 1 -test: 'off' -deploy: 'off' -os: Visual Studio 2017 Preview diff --git a/README.md b/README.md index e584eaef12..e99bc694b5 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ Razor ===== -AppVeyor: [![AppVeyor](https://ci.appveyor.com/api/projects/status/olbc8ur2jna0v27j/branch/dev?svg=true)](https://ci.appveyor.com/project/aspnetci/razor/branch/dev) - Travis: [![Travis](https://travis-ci.org/aspnet/Razor.svg?branch=dev)](https://travis-ci.org/aspnet/Razor) The Razor syntax provides a fast, terse, clean and lightweight way to combine server code with HTML to create dynamic web content. This repo contains the parser and the C# code generator for the Razor syntax. diff --git a/Razor.sln b/Razor.sln index d24fb58587..6ec8cff6c0 100644 --- a/Razor.sln +++ b/Razor.sln @@ -1,6 +1,7 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 + +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.27107.3000 +VisualStudioVersion = 15.0.27130.2036 MinimumVisualStudioVersion = 15.0.26730.03 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{3C0D6505-79B3-49D0-B4C3-176F0F1836ED}" ProjectSection(SolutionItems) = preProject @@ -92,9 +93,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CodeAnalysis.Razo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Razor.Tools", "src\Microsoft.AspNetCore.Razor.Tools\Microsoft.AspNetCore.Razor.Tools.csproj", "{3E7F2D49-3B45-45A8-9893-F73EC1EEBAAB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.NET.Sdk.Razor", "src\Microsoft.NET.Sdk.Razor\Microsoft.NET.Sdk.Razor.csproj", "{7D9ECCEE-71D1-4A42-ABEE-876AFA1B4FC9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.NET.Sdk.Razor", "src\Microsoft.NET.Sdk.Razor\Microsoft.NET.Sdk.Razor.csproj", "{7D9ECCEE-71D1-4A42-ABEE-876AFA1B4FC9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Razor.Tools.Test", "test\Microsoft.AspNetCore.Razor.Tools.Test\Microsoft.AspNetCore.Razor.Tools.Test.csproj", "{6EA56B2B-89EC-4C38-A384-97D203375B06}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Razor.Tools.Test", "test\Microsoft.AspNetCore.Razor.Tools.Test\Microsoft.AspNetCore.Razor.Tools.Test.csproj", "{6EA56B2B-89EC-4C38-A384-97D203375B06}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib", "test\Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib\Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib.csproj", "{72E89155-86C7-454E-BDD9-39F497F2F61B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -398,6 +401,14 @@ Global {6EA56B2B-89EC-4C38-A384-97D203375B06}.Release|Any CPU.Build.0 = Release|Any CPU {6EA56B2B-89EC-4C38-A384-97D203375B06}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU {6EA56B2B-89EC-4C38-A384-97D203375B06}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU + {72E89155-86C7-454E-BDD9-39F497F2F61B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72E89155-86C7-454E-BDD9-39F497F2F61B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72E89155-86C7-454E-BDD9-39F497F2F61B}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU + {72E89155-86C7-454E-BDD9-39F497F2F61B}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU + {72E89155-86C7-454E-BDD9-39F497F2F61B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {72E89155-86C7-454E-BDD9-39F497F2F61B}.Release|Any CPU.Build.0 = Release|Any CPU + {72E89155-86C7-454E-BDD9-39F497F2F61B}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU + {72E89155-86C7-454E-BDD9-39F497F2F61B}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -440,6 +451,7 @@ Global {3E7F2D49-3B45-45A8-9893-F73EC1EEBAAB} = {3C0D6505-79B3-49D0-B4C3-176F0F1836ED} {7D9ECCEE-71D1-4A42-ABEE-876AFA1B4FC9} = {3C0D6505-79B3-49D0-B4C3-176F0F1836ED} {6EA56B2B-89EC-4C38-A384-97D203375B06} = {92463391-81BE-462B-AC3C-78C6C760741F} + {72E89155-86C7-454E-BDD9-39F497F2F61B} = {92463391-81BE-462B-AC3C-78C6C760741F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0035341D-175A-4D05-95E6-F1C2785A1E26} diff --git a/benchmarks/Microsoft.AspNetCore.Razor.Performance/CodeGenerationBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Razor.Performance/CodeGenerationBenchmark.cs index b4320782e6..c35e1db42f 100644 --- a/benchmarks/Microsoft.AspNetCore.Razor.Performance/CodeGenerationBenchmark.cs +++ b/benchmarks/Microsoft.AspNetCore.Razor.Performance/CodeGenerationBenchmark.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; using System.IO; using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.Mvc.Razor.Extensions; @@ -21,33 +20,22 @@ namespace Microsoft.AspNetCore.Razor.Performance } var root = current; - - var engine = RazorEngine.Create(b => { RazorExtensions.Register(b); }); - var fileSystem = RazorProjectFileSystem.Create(root.FullName); + + ProjectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, fileSystem, b => RazorExtensions.Register(b)); ; - DesignTimeTemplateEngine = new MvcRazorTemplateEngine(RazorEngine.CreateDesignTime(b => { RazorExtensions.Register(b); }), fileSystem); - RuntimeTemplateEngine = new MvcRazorTemplateEngine(RazorEngine.Create(b => { RazorExtensions.Register(b); }), fileSystem); - - var codeDocument = RuntimeTemplateEngine.CreateCodeDocument(Path.Combine(root.FullName, "MSN.cshtml")); - - Imports = codeDocument.Imports; - MSN = codeDocument.Source; + MSN = fileSystem.GetItem(Path.Combine(root.FullName, "MSN.cshtml")); } - public RazorTemplateEngine DesignTimeTemplateEngine { get; } + public RazorProjectEngine ProjectEngine { get; } - public RazorTemplateEngine RuntimeTemplateEngine { get; } - - public IReadOnlyList Imports { get; } - - public RazorSourceDocument MSN { get; } + public RazorProjectItem MSN { get; } [Benchmark(Description = "Razor Design Time Code Generation of MSN.com")] public void CodeGeneration_DesignTime_LargeStaticFile() { - var codeDocument = RazorCodeDocument.Create(MSN, Imports); - var generated = DesignTimeTemplateEngine.GenerateCode(codeDocument); + var codeDocument = ProjectEngine.ProcessDesignTime(MSN); + var generated = codeDocument.GetCSharpDocument(); if (generated.Diagnostics.Count != 0) { @@ -58,8 +46,8 @@ namespace Microsoft.AspNetCore.Razor.Performance [Benchmark(Description = "Razor Runtime Code Generation of MSN.com")] public void CodeGeneration_Runtime_LargeStaticFile() { - var codeDocument = RazorCodeDocument.Create(MSN, Imports); - var generated = RuntimeTemplateEngine.GenerateCode(codeDocument); + var codeDocument = ProjectEngine.Process(MSN); + var generated = codeDocument.GetCSharpDocument(); if (generated.Diagnostics.Count != 0) { diff --git a/build/dependencies.props b/build/dependencies.props index 898bb3fff8..d2659358ff 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -4,23 +4,23 @@ 0.10.11 - 2.1.0-preview2-15698 - 2.1.0-preview2-30077 - 2.1.0-preview2-30077 - 2.1.0-preview2-30077 - 15.3.409 - 15.3.409 - 15.3.409 + 2.1.0-preview2-15726 + 2.1.0-preview2-30230 + 2.1.0-preview2-30230 + 2.1.0-preview2-30230 + 15.6.82 + 15.6.82 + 15.6.82 2.6.1 2.6.1 - 2.1.0-preview2-30077 - 2.1.0-preview2-30077 + 2.1.0-preview2-30230 + 2.1.0-preview2-30230 2.1.0-preview2-25711-01 - 2.1.0-preview2-30077 - 2.1.0-preview2-30077 + 2.1.0-preview2-30230 + 2.1.0-preview2-30230 2.0.0 - 2.1.0-preview2-26130-04 - 15.3.0 + 2.1.0-preview2-26225-03 + 15.6.0 15.0.26606 15.6.161-preview 15.6.161-preview @@ -42,8 +42,8 @@ 2.0.1 10.0.1 1.1.92 - 4.5.0-preview2-26130-01 - 4.5.0-preview2-26130-01 + 4.5.0-preview2-26224-02 + 4.5.0-preview2-26224-02 9.0.1 2.7.0-beta3-62512-06 2.7.0-beta3-62512-06 diff --git a/build/sources.props b/build/sources.props index b97c0c5cfb..02efac4549 100644 --- a/build/sources.props +++ b/build/sources.props @@ -1,10 +1,11 @@ - + $(DotNetRestoreSources) $(RestoreSources); + https://dotnet.myget.org/F/dotnet-core/api/v3/index.json; https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json; https://dotnet.myget.org/F/aspnetcore-tools/api/v3/index.json; https://dotnet.myget.org/F/msbuild/api/v3/index.json; diff --git a/korebuild-lock.txt b/korebuild-lock.txt index c6125ba391..ad1d7d3c02 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-preview2-15698 -commithash:7216e5068cb1957e09d45fcbe58a744dd5c2de73 +version:2.1.0-preview2-15726 +commithash:599e691c41f502ed9e062b1822ce13b673fc916e diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/DefaultMvcImportFeature.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/DefaultMvcImportFeature.cs deleted file mode 100644 index 055f306eb5..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/DefaultMvcImportFeature.cs +++ /dev/null @@ -1,84 +0,0 @@ -// 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; -using Microsoft.AspNetCore.Razor.Language; - -namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X -{ - internal class DefaultMvcImportFeature : RazorProjectEngineFeatureBase, IRazorImportFeature - { - private const string ImportsFileName = "_ViewImports.cshtml"; - - public IReadOnlyList GetImports(RazorProjectItem projectItem) - { - if (projectItem == null) - { - throw new ArgumentNullException(nameof(projectItem)); - } - - var imports = new List(); - AddDefaultDirectivesImport(imports); - - // We add hierarchical imports second so any default directive imports can be overridden. - AddHierarchicalImports(projectItem, imports); - - return imports; - } - - // Internal for testing - internal static void AddDefaultDirectivesImport(List imports) - { - using (var stream = new MemoryStream()) - using (var writer = new StreamWriter(stream, Encoding.UTF8)) - { - writer.WriteLine("@using System"); - writer.WriteLine("@using System.Collections.Generic"); - writer.WriteLine("@using System.Linq"); - writer.WriteLine("@using System.Threading.Tasks"); - writer.WriteLine("@using Microsoft.AspNetCore.Mvc"); - writer.WriteLine("@using Microsoft.AspNetCore.Mvc.Rendering"); - writer.WriteLine("@using Microsoft.AspNetCore.Mvc.ViewFeatures"); - writer.WriteLine("@inject global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper Html"); - writer.WriteLine("@inject global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json"); - writer.WriteLine("@inject global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component"); - writer.WriteLine("@inject global::Microsoft.AspNetCore.Mvc.IUrlHelper Url"); - writer.WriteLine("@inject global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider"); - writer.WriteLine("@addTagHelper Microsoft.AspNetCore.Mvc.Razor.TagHelpers.UrlResolutionTagHelper, Microsoft.AspNetCore.Mvc.Razor"); - writer.Flush(); - - stream.Position = 0; - var defaultMvcImports = RazorSourceDocument.ReadFrom(stream, fileName: null, encoding: Encoding.UTF8); - imports.Add(defaultMvcImports); - } - } - - // Internal for testing - internal void AddHierarchicalImports(RazorProjectItem projectItem, List imports) - { - // We want items in descending order. FindHierarchicalItems returns items in ascending order. - var importProjectItems = ProjectEngine.FileSystem.FindHierarchicalItems(projectItem.FilePath, ImportsFileName).Reverse(); - foreach (var importProjectItem in importProjectItems) - { - RazorSourceDocument importSourceDocument; - - if (importProjectItem.Exists) - { - importSourceDocument = RazorSourceDocument.ReadFrom(importProjectItem); - } - else - { - // File doesn't exist on disk so just add a marker source document as an identifier for "there could be something here". - var sourceDocumentProperties = new RazorSourceDocumentProperties(importProjectItem.FilePath, importProjectItem.RelativePhysicalPath); - importSourceDocument = RazorSourceDocument.Create(string.Empty, sourceDocumentProperties); - } - - imports.Add(importSourceDocument); - } - } - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/ModelDirective.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/ModelDirective.cs index a2bce9974d..0c5859ce22 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/ModelDirective.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/ModelDirective.cs @@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X } builder.AddDirective(Directive); - builder.Features.Add(new Pass(builder.Configuration.DesignTime)); + builder.Features.Add(new Pass()); return builder; } @@ -64,13 +64,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X internal class Pass : IntermediateNodePassBase, IRazorDirectiveClassifierPass { - private readonly bool _designTime; - - public Pass(bool designTime) - { - _designTime = designTime; - } - // Runs after the @inherits directive public override int Order => 5; @@ -79,7 +72,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X var visitor = new Visitor(); var modelType = GetModelType(documentNode, visitor); - if (_designTime) + if (documentNode.Options.DesignTime) { // Alias the TModel token to a known type. // This allows design time compilation to succeed for Razor files where the token isn't replaced. @@ -143,7 +136,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X } builder.AddDirective(Directive); - builder.Features.Add(new Pass(builder.DesignTime)); + builder.Features.Add(new Pass()); return builder; } #endregion diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/MvcImportProjectFeature.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/MvcImportProjectFeature.cs new file mode 100644 index 0000000000..254c57c1c4 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/MvcImportProjectFeature.cs @@ -0,0 +1,89 @@ +// 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; +using Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X +{ + internal class MvcImportProjectFeature : RazorProjectEngineFeatureBase, IImportProjectFeature + { + private const string ImportsFileName = "_ViewImports.cshtml"; + + public IReadOnlyList GetImports(RazorProjectItem projectItem) + { + if (projectItem == null) + { + throw new ArgumentNullException(nameof(projectItem)); + } + + var imports = new List(); + AddDefaultDirectivesImport(imports); + + // We add hierarchical imports second so any default directive imports can be overridden. + AddHierarchicalImports(projectItem, imports); + + return imports; + } + + // Internal for testing + internal static void AddDefaultDirectivesImport(List imports) + { + imports.Add(DefaultDirectivesProjectItem.Instance); + } + + // Internal for testing + internal void AddHierarchicalImports(RazorProjectItem projectItem, List imports) + { + // We want items in descending order. FindHierarchicalItems returns items in ascending order. + var importProjectItems = ProjectEngine.FileSystem.FindHierarchicalItems(projectItem.FilePath, ImportsFileName).Reverse(); + imports.AddRange(importProjectItems); + } + + private class DefaultDirectivesProjectItem : RazorProjectItem + { + private readonly byte[] _defaultImportBytes; + + private DefaultDirectivesProjectItem() + { + var preamble = Encoding.UTF8.GetPreamble(); + var content = @" +@using System +@using System.Collections.Generic +@using System.Linq +@using System.Threading.Tasks +@using Microsoft.AspNetCore.Mvc +@using Microsoft.AspNetCore.Mvc.Rendering +@using Microsoft.AspNetCore.Mvc.ViewFeatures +@inject global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper Html +@inject global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json +@inject global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component +@inject global::Microsoft.AspNetCore.Mvc.IUrlHelper Url +@inject global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider +@addTagHelper Microsoft.AspNetCore.Mvc.Razor.TagHelpers.UrlResolutionTagHelper, Microsoft.AspNetCore.Mvc.Razor +"; + var contentBytes = Encoding.UTF8.GetBytes(content); + + _defaultImportBytes = new byte[preamble.Length + contentBytes.Length]; + preamble.CopyTo(_defaultImportBytes, 0); + contentBytes.CopyTo(_defaultImportBytes, preamble.Length); + } + + public override string BasePath => null; + + public override string FilePath => null; + + public override string PhysicalPath => null; + + public override bool Exists => true; + + public static DefaultDirectivesProjectItem Instance { get; } = new DefaultDirectivesProjectItem(); + + public override Stream Read() => new MemoryStream(_defaultImportBytes); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/RazorExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/RazorExtensions.cs index f94cf52abd..e8b4f0018c 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/RazorExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/RazorExtensions.cs @@ -16,8 +16,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X throw new ArgumentNullException(nameof(builder)); } - EnsureDesignTime(builder); - InjectDirective.Register(builder); ModelDirective.Register(builder); @@ -37,7 +35,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X builder.Features.Add(new ModelExpressionPass()); builder.Features.Add(new MvcViewDocumentClassifierPass()); - builder.SetImportFeature(new DefaultMvcImportFeature()); + builder.SetImportFeature(new MvcImportProjectFeature()); } public static void RegisterViewComponentTagHelpers(RazorProjectEngineBuilder builder) @@ -47,22 +45,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X throw new ArgumentNullException(nameof(builder)); } - EnsureDesignTime(builder); - builder.Features.Add(new ViewComponentTagHelperPass()); builder.AddTargetExtension(new ViewComponentTagHelperTargetExtension()); } - private static void EnsureDesignTime(RazorProjectEngineBuilder builder) - { - if (builder.Configuration.DesignTime) - { - return; - } - - throw new NotSupportedException(Resources.RuntimeCodeGenerationNotSupported); - } - #region Obsolete public static void Register(IRazorEngineBuilder builder) { diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/AssemblyAttributeInjectionPass.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/AssemblyAttributeInjectionPass.cs index 1123b1a31f..cf5549d0f5 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/AssemblyAttributeInjectionPass.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/AssemblyAttributeInjectionPass.cs @@ -14,6 +14,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) { + if (documentNode.Options.DesignTime) + { + return; + } + var @namespace = documentNode.FindPrimaryNamespace(); if (@namespace == null || string.IsNullOrEmpty(@namespace.Content)) { diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/DefaultMvcImportFeature.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/DefaultMvcImportFeature.cs deleted file mode 100644 index 4852951e33..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/DefaultMvcImportFeature.cs +++ /dev/null @@ -1,86 +0,0 @@ -// 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; -using Microsoft.AspNetCore.Razor.Language; - -namespace Microsoft.AspNetCore.Mvc.Razor.Extensions -{ - internal class DefaultMvcImportFeature : RazorProjectEngineFeatureBase, IRazorImportFeature - { - private const string ImportsFileName = "_ViewImports.cshtml"; - - public IReadOnlyList GetImports(RazorProjectItem projectItem) - { - if (projectItem == null) - { - throw new ArgumentNullException(nameof(projectItem)); - } - - var imports = new List(); - AddDefaultDirectivesImport(imports); - - // We add hierarchical imports second so any default directive imports can be overridden. - AddHierarchicalImports(projectItem, imports); - - return imports; - } - - // Internal for testing - internal static void AddDefaultDirectivesImport(List imports) - { - using (var stream = new MemoryStream()) - using (var writer = new StreamWriter(stream, Encoding.UTF8)) - { - writer.WriteLine("@using System"); - writer.WriteLine("@using System.Collections.Generic"); - writer.WriteLine("@using System.Linq"); - writer.WriteLine("@using System.Threading.Tasks"); - writer.WriteLine("@using Microsoft.AspNetCore.Mvc"); - writer.WriteLine("@using Microsoft.AspNetCore.Mvc.Rendering"); - writer.WriteLine("@using Microsoft.AspNetCore.Mvc.ViewFeatures"); - writer.WriteLine("@inject global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper Html"); - writer.WriteLine("@inject global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json"); - writer.WriteLine("@inject global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component"); - writer.WriteLine("@inject global::Microsoft.AspNetCore.Mvc.IUrlHelper Url"); - writer.WriteLine("@inject global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider"); - writer.WriteLine("@addTagHelper Microsoft.AspNetCore.Mvc.Razor.TagHelpers.UrlResolutionTagHelper, Microsoft.AspNetCore.Mvc.Razor"); - writer.WriteLine("@addTagHelper Microsoft.AspNetCore.Mvc.Razor.TagHelpers.HeadTagHelper, Microsoft.AspNetCore.Mvc.Razor"); - writer.WriteLine("@addTagHelper Microsoft.AspNetCore.Mvc.Razor.TagHelpers.BodyTagHelper, Microsoft.AspNetCore.Mvc.Razor"); - writer.Flush(); - - stream.Position = 0; - var defaultMvcImports = RazorSourceDocument.ReadFrom(stream, fileName: null, encoding: Encoding.UTF8); - imports.Add(defaultMvcImports); - } - } - - // Internal for testing - internal void AddHierarchicalImports(RazorProjectItem projectItem, List imports) - { - // We want items in descending order. FindHierarchicalItems returns items in ascending order. - var importProjectItems = ProjectEngine.FileSystem.FindHierarchicalItems(projectItem.FilePath, ImportsFileName).Reverse(); - foreach (var importProjectItem in importProjectItems) - { - RazorSourceDocument importSourceDocument; - - if (importProjectItem.Exists) - { - importSourceDocument = RazorSourceDocument.ReadFrom(importProjectItem); - } - else - { - // File doesn't exist on disk so just add a marker source document as an identifier for "there could be something here". - var sourceDocumentProperties = new RazorSourceDocumentProperties(importProjectItem.FilePath, importProjectItem.RelativePhysicalPath); - importSourceDocument = RazorSourceDocument.Create(string.Empty, sourceDocumentProperties); - } - - imports.Add(importSourceDocument); - } - } - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ExtensionInitializer.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ExtensionInitializer.cs new file mode 100644 index 0000000000..eeb3246f74 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ExtensionInitializer.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions +{ + internal class ExtensionInitializer : RazorExtensionInitializer + { + public override void Initialize(RazorProjectEngineBuilder builder) + { + RazorExtensions.Register(builder); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/InstrumentationPass.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/InstrumentationPass.cs index 9f271faf82..1ae0a72210 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/InstrumentationPass.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/InstrumentationPass.cs @@ -15,6 +15,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) { + if (documentNode.Options.DesignTime) + { + return; + } + var walker = new Visitor(); walker.VisitDocument(documentNode); diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ModelDirective.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ModelDirective.cs index 9c16b96e57..b6e2955f22 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ModelDirective.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ModelDirective.cs @@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions } builder.AddDirective(Directive); - builder.Features.Add(new Pass(builder.Configuration.DesignTime)); + builder.Features.Add(new Pass()); return builder; } @@ -71,13 +71,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions internal class Pass : IntermediateNodePassBase, IRazorDirectiveClassifierPass { - private readonly bool _designTime; - - public Pass(bool designTime) - { - _designTime = designTime; - } - // Runs after the @inherits directive public override int Order => 5; @@ -86,7 +79,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions var visitor = new Visitor(); var modelType = GetModelType(documentNode, visitor); - if (_designTime) + if (documentNode.Options.DesignTime) { // Alias the TModel token to a known type. // This allows design time compilation to succeed for Razor files where the token isn't replaced. @@ -150,7 +143,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions } builder.AddDirective(Directive); - builder.Features.Add(new Pass(builder.DesignTime)); + builder.Features.Add(new Pass()); return builder; } #endregion diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/MvcImportProjectFeature.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/MvcImportProjectFeature.cs new file mode 100644 index 0000000000..5c6ee7361d --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/MvcImportProjectFeature.cs @@ -0,0 +1,91 @@ +// 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; +using Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions +{ + internal class MvcImportProjectFeature : RazorProjectEngineFeatureBase, IImportProjectFeature + { + private const string ImportsFileName = "_ViewImports.cshtml"; + + public IReadOnlyList GetImports(RazorProjectItem projectItem) + { + if (projectItem == null) + { + throw new ArgumentNullException(nameof(projectItem)); + } + + var imports = new List(); + AddDefaultDirectivesImport(imports); + + // We add hierarchical imports second so any default directive imports can be overridden. + AddHierarchicalImports(projectItem, imports); + + return imports; + } + + // Internal for testing + internal static void AddDefaultDirectivesImport(List imports) + { + imports.Add(DefaultDirectivesProjectItem.Instance); + } + + // Internal for testing + internal void AddHierarchicalImports(RazorProjectItem projectItem, List imports) + { + // We want items in descending order. FindHierarchicalItems returns items in ascending order. + var importProjectItems = ProjectEngine.FileSystem.FindHierarchicalItems(projectItem.FilePath, ImportsFileName).Reverse(); + imports.AddRange(importProjectItems); + } + + private class DefaultDirectivesProjectItem : RazorProjectItem + { + private readonly byte[] _defaultImportBytes; + + private DefaultDirectivesProjectItem() + { + var preamble = Encoding.UTF8.GetPreamble(); + var content = @" +@using System +@using System.Collections.Generic +@using System.Linq +@using System.Threading.Tasks +@using Microsoft.AspNetCore.Mvc +@using Microsoft.AspNetCore.Mvc.Rendering +@using Microsoft.AspNetCore.Mvc.ViewFeatures +@inject global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper Html +@inject global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json +@inject global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component +@inject global::Microsoft.AspNetCore.Mvc.IUrlHelper Url +@inject global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider +@addTagHelper Microsoft.AspNetCore.Mvc.Razor.TagHelpers.UrlResolutionTagHelper, Microsoft.AspNetCore.Mvc.Razor +@addTagHelper Microsoft.AspNetCore.Mvc.Razor.TagHelpers.HeadTagHelper, Microsoft.AspNetCore.Mvc.Razor +@addTagHelper Microsoft.AspNetCore.Mvc.Razor.TagHelpers.BodyTagHelper, Microsoft.AspNetCore.Mvc.Razor +"; + var contentBytes = Encoding.UTF8.GetBytes(content); + + _defaultImportBytes = new byte[preamble.Length + contentBytes.Length]; + preamble.CopyTo(_defaultImportBytes, 0); + contentBytes.CopyTo(_defaultImportBytes, preamble.Length); + } + + public override string BasePath => null; + + public override string FilePath => null; + + public override string PhysicalPath => null; + + public override bool Exists => true; + + public static DefaultDirectivesProjectItem Instance { get; } = new DefaultDirectivesProjectItem(); + + public override Stream Read() => new MemoryStream(_defaultImportBytes); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Properties/AssemblyInfo.cs index 6be3a1f170..b5caca17dd 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Properties/AssemblyInfo.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Properties/AssemblyInfo.cs @@ -2,6 +2,11 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Runtime.CompilerServices; +using Microsoft.AspNetCore.Mvc.Razor.Extensions; +using Microsoft.AspNetCore.Razor.Language; + +[assembly: ProvideRazorExtensionInitializer("MVC-2.0", typeof(ExtensionInitializer))] +[assembly: ProvideRazorExtensionInitializer("MVC-2.1", typeof(ExtensionInitializer))] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Razor.Extensions.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.VisualStudio.Editor.Razor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/RazorExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/RazorExtensions.cs index ac1d8b35e9..abf1b447f0 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/RazorExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/RazorExtensions.cs @@ -25,6 +25,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions InheritsDirective.Register(builder); SectionDirective.Register(builder); + builder.Features.Add(new ViewComponentTagHelperDescriptorProvider()); + builder.AddTargetExtension(new ViewComponentTagHelperTargetExtension()); builder.AddTargetExtension(new TemplateTargetExtension() { @@ -36,14 +38,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions builder.Features.Add(new ViewComponentTagHelperPass()); builder.Features.Add(new RazorPageDocumentClassifierPass()); builder.Features.Add(new MvcViewDocumentClassifierPass()); + builder.Features.Add(new AssemblyAttributeInjectionPass()); + builder.Features.Add(new InstrumentationPass()); - if (!builder.Configuration.DesignTime) - { - builder.Features.Add(new AssemblyAttributeInjectionPass()); - builder.Features.Add(new InstrumentationPass()); - } - - builder.SetImportFeature(new DefaultMvcImportFeature()); + builder.SetImportFeature(new MvcImportProjectFeature()); } #region Obsolete diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.props b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.Extensions.props similarity index 75% rename from src/Microsoft.AspNetCore.Mvc.Razor.Extensions/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.props rename to src/Microsoft.AspNetCore.Mvc.Razor.Extensions/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.Extensions.props index 8d2ac3b630..ef8766cdf2 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.props +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.Extensions.props @@ -10,6 +10,9 @@ Set the primary configuration supported by this pacakge as the default configuration for Razor. --> MVC-2.1 + + + <_MvcExtensionAssemblyPath Condition="'$(_MvcExtensionAssemblyPath)'==''">$(MSBuildThisFileDirectory)..\..\lib\netstandard2.0\Microsoft.AspNetCore.Mvc.Razor.Extensions.dll @@ -26,7 +29,7 @@ Microsoft.AspNetCore.Mvc.Razor.Extensions - $(MSBuildThisFileDirectory)..\..\lib\netstandard2.0\Microsoft.AspNetCore.Mvc.Razor.Extensions.dll + $(_MvcExtensionAssemblyPath) \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.targets b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.Extensions.targets similarity index 53% rename from src/Microsoft.AspNetCore.Mvc.Razor.Extensions/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.targets rename to src/Microsoft.AspNetCore.Mvc.Razor.Extensions/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.Extensions.targets index 61a0e7a8dc..418a3f1b01 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.targets +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.Extensions.targets @@ -5,5 +5,10 @@ MVC will generally want to add support for runtime compilation, but only for applications. --> true + + + .Views - \ No newline at end of file + diff --git a/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.CodeGeneration.targets b/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.CodeGeneration.targets index 699b4608f3..1eb7d2baad 100644 --- a/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.CodeGeneration.targets +++ b/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.CodeGeneration.targets @@ -60,11 +60,11 @@ --> - - + AlwaysCreate="true" /> + + + + @@ -121,6 +124,9 @@ UseServer="$(UseRazorBuildServer)" ForceServer="$(_RazorForceBuildServer)" PipeName="$(_RazorBuildServerPipeName)" + Version="$(RazorLangVersion)" + Configuration="@(ResolvedRazorConfiguration)" + Extensions="@(ResolvedRazorExtension)" Sources="@(RazorGenerateWithTargetPath)" ProjectRoot="$(MSBuildProjectDirectory)" TagHelperManifest="$(_RazorTagHelperOutputCache)" /> diff --git a/src/Microsoft.AspNetCore.Razor.Language/AssemblyExtension.cs b/src/Microsoft.AspNetCore.Razor.Language/AssemblyExtension.cs new file mode 100644 index 0000000000..97f1c31c15 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Language/AssemblyExtension.cs @@ -0,0 +1,55 @@ +// 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.Reflection; + +namespace Microsoft.AspNetCore.Razor.Language +{ + public class AssemblyExtension : RazorExtension + { + public AssemblyExtension(string extensionName, Assembly assembly) + { + if (extensionName == null) + { + throw new ArgumentNullException(nameof(extensionName)); + } + + if (assembly == null) + { + throw new ArgumentNullException(nameof(assembly)); + } + + ExtensionName = extensionName; + Assembly = assembly; + } + + public override string ExtensionName { get; } + + public Assembly Assembly { get; } + + internal RazorExtensionInitializer CreateInitializer() + { + // It's not an error to have an assembly with no initializers. This is useful to specify a dependency + // that doesn't really provide any Razor configuration. + var attributes = Assembly.GetCustomAttributes(); + foreach (var attribute in attributes) + { + // Using extension names and requiring them to line up allows a single assembly to ship multiple + // extensions/initializers for different configurations. + if (!string.Equals(attribute.ExtensionName, ExtensionName, StringComparison.Ordinal)) + { + continue; + } + + // There's no real protection/exception handling here because this set isn't really user-extensible + // right now. This would be a great place to add some additional diagnostics and hardening in the + // future. + var initializer = (RazorExtensionInitializer)Activator.CreateInstance(attribute.InitializerType); + return initializer; + } + + return null; + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/CodeWriterExtensions.cs b/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/CodeWriterExtensions.cs index 7d8c846ca2..e6a749b269 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/CodeWriterExtensions.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/CodeWriterExtensions.cs @@ -540,7 +540,7 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration public CSharpCodeWritingScope(CodeWriter writer, int tabSize = 4, bool autoSpace = true) { _writer = writer; - _autoSpace = true; + _autoSpace = autoSpace; _tabSize = tabSize; _startIndent = -1; // Set in WriteStartScope diff --git a/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/DefaultDocumentWriter.cs b/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/DefaultDocumentWriter.cs index 772c5a0fab..09ed47b6fa 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/DefaultDocumentWriter.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/DefaultDocumentWriter.cs @@ -159,11 +159,35 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration Context.CodeWriter.Write(" "); } - Context.CodeWriter - .Write(node.ReturnType) - .Write(" ") - .Write(node.MethodName) - .WriteLine("()"); + Context.CodeWriter.Write(node.ReturnType); + Context.CodeWriter.Write(" "); + + Context.CodeWriter.Write(node.MethodName); + Context.CodeWriter.Write("("); + + for (var i = 0; i < node.Parameters.Count; i++) + { + var parameter = node.Parameters[i]; + + for (var j = 0; j < parameter.Modifiers.Count; j++) + { + Context.CodeWriter.Write(parameter.Modifiers[j]); + Context.CodeWriter.Write(" "); + } + + Context.CodeWriter.Write(parameter.TypeName); + Context.CodeWriter.Write(" "); + + Context.CodeWriter.Write(parameter.ParameterName); + + if (i < node.Parameters.Count - 1) + { + Context.CodeWriter.Write(", "); + } + } + + Context.CodeWriter.Write(")"); + Context.CodeWriter.WriteLine(); using (Context.CodeWriter.BuildScope()) { diff --git a/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/DocumentWriter.cs b/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/DocumentWriter.cs index fcfa014da2..68f7ddffc0 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/DocumentWriter.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/DocumentWriter.cs @@ -2,12 +2,30 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.ComponentModel; using Microsoft.AspNetCore.Razor.Language.Intermediate; namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration { public abstract class DocumentWriter { + public static DocumentWriter CreateDefault(CodeTarget codeTarget, RazorCodeGenerationOptions options) + { + if (codeTarget == null) + { + throw new ArgumentNullException(nameof(codeTarget)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + return new DefaultDocumentWriter(codeTarget, options); + } + + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This method was intended to be static, use CreateDefault instead.")] public DocumentWriter Create(CodeTarget codeTarget, RazorCodeGenerationOptions options) { if (codeTarget == null) diff --git a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorImportFeature.cs b/src/Microsoft.AspNetCore.Razor.Language/DefaultImportProjectFeature.cs similarity index 55% rename from src/Microsoft.AspNetCore.Razor.Language/DefaultRazorImportFeature.cs rename to src/Microsoft.AspNetCore.Razor.Language/DefaultImportProjectFeature.cs index ea626e97e0..92778322d7 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorImportFeature.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/DefaultImportProjectFeature.cs @@ -6,8 +6,8 @@ using System.Collections.Generic; namespace Microsoft.AspNetCore.Razor.Language { - internal class DefaultRazorImportFeature : RazorProjectEngineFeatureBase, IRazorImportFeature + internal class DefaultImportProjectFeature : RazorProjectEngineFeatureBase, IImportProjectFeature { - public IReadOnlyList GetImports(RazorProjectItem projectItem) => Array.Empty(); + public IReadOnlyList GetImports(RazorProjectItem projectItem) => Array.Empty(); } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorCSharpLoweringPhase.cs b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorCSharpLoweringPhase.cs index 4f9ff9f151..f09c68069a 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorCSharpLoweringPhase.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorCSharpLoweringPhase.cs @@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Razor.Language throw new InvalidOperationException(message); } - var writer = new DefaultDocumentWriter(documentNode.Target, documentNode.Options); + var writer = DocumentWriter.CreateDefault(documentNode.Target, documentNode.Options); var cSharpDocument = writer.WriteDocument(codeDocument, documentNode); codeDocument.SetCSharpDocument(cSharpDocument); } diff --git a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorCodeGenerationOptionsBuilder.cs b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorCodeGenerationOptionsBuilder.cs index ad3dc7b83d..8dd095a04e 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorCodeGenerationOptionsBuilder.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorCodeGenerationOptionsBuilder.cs @@ -1,16 +1,30 @@ // 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.AspNetCore.Razor.Language { internal class DefaultRazorCodeGenerationOptionsBuilder : RazorCodeGenerationOptionsBuilder { - public DefaultRazorCodeGenerationOptionsBuilder(bool designTime) + private bool _designTime; + + public DefaultRazorCodeGenerationOptionsBuilder(RazorConfiguration configuration) { - DesignTime = designTime; + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } } - public override bool DesignTime { get; } + public DefaultRazorCodeGenerationOptionsBuilder(bool designTime) + { + _designTime = designTime; + } + + public override RazorConfiguration Configuration { get; } + + public override bool DesignTime => _designTime; public override int IndentSize { get; set; } = 4; @@ -22,5 +36,10 @@ namespace Microsoft.AspNetCore.Razor.Language { return new DefaultRazorCodeGenerationOptions(IndentWithTabs, IndentSize, DesignTime, SuppressChecksum, SuppressMetadataAttributes); } + + public override void SetDesignTime(bool designTime) + { + _designTime = designTime; + } } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorCodeGenerationOptionsFactoryProjectFeature.cs b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorCodeGenerationOptionsFactoryProjectFeature.cs new file mode 100644 index 0000000000..db2a25b81f --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorCodeGenerationOptionsFactoryProjectFeature.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; + +namespace Microsoft.AspNetCore.Razor.Language +{ + internal class DefaultRazorCodeGenerationOptionsFactoryProjectFeature : RazorProjectEngineFeatureBase, IRazorCodeGenerationOptionsFactoryProjectFeature + { + private IConfigureRazorCodeGenerationOptionsFeature[] _configureOptions; + + protected override void OnInitialized() + { + _configureOptions = ProjectEngine.EngineFeatures.OfType().ToArray(); + } + + public RazorCodeGenerationOptions Create(Action configure) + { + var builder = new DefaultRazorCodeGenerationOptionsBuilder(ProjectEngine.Configuration); + configure?.Invoke(builder); + + for (var i = 0; i < _configureOptions.Length; i++) + { + _configureOptions[i].Configure(builder); + } + + var options = builder.Build(); + return options; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorCodeGenerationOptionsFeature.cs b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorCodeGenerationOptionsFeature.cs index 0238ee2d76..1c915364d7 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorCodeGenerationOptionsFeature.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorCodeGenerationOptionsFeature.cs @@ -5,7 +5,9 @@ using System.Linq; namespace Microsoft.AspNetCore.Razor.Language { +#pragma warning disable CS0618 // Type or member is obsolete internal class DefaultRazorCodeGenerationOptionsFeature : RazorEngineFeatureBase, IRazorCodeGenerationOptionsFeature +#pragma warning restore CS0618 // Type or member is obsolete { private readonly bool _designTime; private IConfigureRazorCodeGenerationOptionsFeature[] _configureOptions; diff --git a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorIntermediateNodeLoweringPhase.cs b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorIntermediateNodeLoweringPhase.cs index b7047bca1b..5e3ae8a258 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorIntermediateNodeLoweringPhase.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorIntermediateNodeLoweringPhase.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Razor.Language.Legacy; namespace Microsoft.AspNetCore.Razor.Language { +#pragma warning disable CS0618 // Type or member is obsolete internal class DefaultRazorIntermediateNodeLoweringPhase : RazorEnginePhaseBase, IRazorIntermediateNodeLoweringPhase { private IRazorCodeGenerationOptionsFeature _optionsFeature; @@ -31,7 +32,7 @@ namespace Microsoft.AspNetCore.Razor.Language var document = new DocumentIntermediateNode(); var builder = IntermediateNodeBuilder.Create(document); - document.Options = _optionsFeature.GetOptions(); + document.Options = codeDocument.GetCodeGenerationOptions() ?? _optionsFeature.GetOptions(); var namespaces = new Dictionary(StringComparer.Ordinal); @@ -785,4 +786,5 @@ namespace Microsoft.AspNetCore.Razor.Language private static bool IsMalformed(List diagnostics) => diagnostics.Count > 0 && diagnostics.Any(diagnostic => diagnostic.Severity == RazorDiagnosticSeverity.Error); } +#pragma warning restore CS0618 // Type or member is obsolete } diff --git a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorParserOptionsBuilder.cs b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorParserOptionsBuilder.cs index eadc773439..2e4d3273d2 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorParserOptionsBuilder.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorParserOptionsBuilder.cs @@ -9,23 +9,43 @@ namespace Microsoft.AspNetCore.Razor.Language { internal class DefaultRazorParserOptionsBuilder : RazorParserOptionsBuilder { - public DefaultRazorParserOptionsBuilder(bool designTime, RazorLanguageVersion version) + private bool _designTime; + + public DefaultRazorParserOptionsBuilder(RazorConfiguration configuration) { - DesignTime = designTime; - Version = version; + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + Configuration = configuration; + LanguageVersion = configuration.LanguageVersion; } - public override bool DesignTime { get; } + public DefaultRazorParserOptionsBuilder(bool designTime, RazorLanguageVersion version) + { + _designTime = designTime; + LanguageVersion = version; + } + + public override RazorConfiguration Configuration { get; } + + public override bool DesignTime => _designTime; public override ICollection Directives { get; } = new List(); public override bool ParseLeadingDirectives { get; set; } - public override RazorLanguageVersion Version { get; } + public override RazorLanguageVersion LanguageVersion { get; } public override RazorParserOptions Build() { - return new DefaultRazorParserOptions(Directives.ToArray(), DesignTime, ParseLeadingDirectives, Version); + return new DefaultRazorParserOptions(Directives.ToArray(), DesignTime, ParseLeadingDirectives, LanguageVersion); + } + + public override void SetDesignTime(bool designTime) + { + _designTime = designTime; } } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorParserOptionsFactoryProjectFeature.cs b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorParserOptionsFactoryProjectFeature.cs new file mode 100644 index 0000000000..32fb04af7f --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorParserOptionsFactoryProjectFeature.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; + +namespace Microsoft.AspNetCore.Razor.Language +{ + internal class DefaultRazorParserOptionsFactoryProjectFeature : RazorProjectEngineFeatureBase, IRazorParserOptionsFactoryProjectFeature + { + private IConfigureRazorParserOptionsFeature[] _configureOptions; + + protected override void OnInitialized() + { + _configureOptions = ProjectEngine.EngineFeatures.OfType().ToArray(); + } + + public RazorParserOptions Create(Action configure) + { + var builder = new DefaultRazorParserOptionsBuilder(ProjectEngine.Configuration); + configure?.Invoke(builder); + + for (var i = 0; i < _configureOptions.Length; i++) + { + _configureOptions[i].Configure(builder); + } + + var options = builder.Build(); + return options; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorParserOptionsFeature.cs b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorParserOptionsFeature.cs index 867e289ee7..eecb9bd9cc 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorParserOptionsFeature.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorParserOptionsFeature.cs @@ -5,7 +5,9 @@ using System.Linq; namespace Microsoft.AspNetCore.Razor.Language { +#pragma warning disable CS0618 // Type or member is obsolete internal class DefaultRazorParserOptionsFeature : RazorEngineFeatureBase, IRazorParserOptionsFeature +#pragma warning restore CS0618 // Type or member is obsolete { private readonly bool _designTime; private readonly RazorLanguageVersion _version; diff --git a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorParsingPhase.cs b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorParsingPhase.cs index 5a13e68760..8264201271 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorParsingPhase.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorParsingPhase.cs @@ -3,6 +3,7 @@ namespace Microsoft.AspNetCore.Razor.Language { +#pragma warning disable CS0618 // Type or member is obsolete internal class DefaultRazorParsingPhase : RazorEnginePhaseBase, IRazorParsingPhase { private IRazorParserOptionsFeature _optionsFeature; @@ -14,7 +15,7 @@ namespace Microsoft.AspNetCore.Razor.Language protected override void ExecuteCore(RazorCodeDocument codeDocument) { - var options = _optionsFeature.GetOptions(); + var options = codeDocument.GetParserOptions() ??_optionsFeature.GetOptions(); var syntaxTree = RazorSyntaxTree.Parse(codeDocument.Source, options); codeDocument.SetSyntaxTree(syntaxTree); @@ -26,4 +27,5 @@ namespace Microsoft.AspNetCore.Razor.Language codeDocument.SetImportSyntaxTrees(importSyntaxTrees); } } +#pragma warning restore CS0618 // Type or member is obsolete } diff --git a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorProjectEngine.cs b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorProjectEngine.cs index 6cac02dc25..89b667979a 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorProjectEngine.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorProjectEngine.cs @@ -10,10 +10,16 @@ namespace Microsoft.AspNetCore.Razor.Language internal class DefaultRazorProjectEngine : RazorProjectEngine { public DefaultRazorProjectEngine( + RazorConfiguration configuration, RazorEngine engine, RazorProjectFileSystem fileSystem, - IReadOnlyList features) + IReadOnlyList projectFeatures) { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + if (engine == null) { throw new ArgumentNullException(nameof(engine)); @@ -24,48 +30,81 @@ namespace Microsoft.AspNetCore.Razor.Language throw new ArgumentNullException(nameof(fileSystem)); } - if (features == null) + if (projectFeatures == null) { - throw new ArgumentNullException(nameof(features)); + throw new ArgumentNullException(nameof(projectFeatures)); } + Configuration = configuration; Engine = engine; FileSystem = fileSystem; - Features = features; + ProjectFeatures = projectFeatures; - for (var i = 0; i < features.Count; i++) + for (var i = 0; i < projectFeatures.Count; i++) { - features[i].ProjectEngine = this; + projectFeatures[i].ProjectEngine = this; } } + public override RazorConfiguration Configuration { get; } + public override RazorProjectFileSystem FileSystem { get; } public override RazorEngine Engine { get; } - public override IReadOnlyList Features { get; } + public override IReadOnlyList ProjectFeatures { get; } - public override RazorCodeDocument Process(RazorProjectItem projectItem) + protected override RazorCodeDocument CreateCodeDocumentCore(RazorProjectItem projectItem) { if (projectItem == null) { throw new ArgumentNullException(nameof(projectItem)); } - var importFeature = GetRequiredFeature(); - var imports = importFeature.GetImports(projectItem); var sourceDocument = RazorSourceDocument.ReadFrom(projectItem); - var codeDocument = RazorCodeDocument.Create(sourceDocument, imports); + var importFeature = GetRequiredFeature(); + var importItems = importFeature.GetImports(projectItem); + var importSourceDocuments = GetImportSourceDocuments(importItems); + + var parserOptions = GetRequiredFeature().Create(ConfigureParserOptions); + var codeGenerationOptions = GetRequiredFeature().Create(ConfigureCodeGenerationOptions); + + return RazorCodeDocument.Create(sourceDocument, importSourceDocuments, parserOptions, codeGenerationOptions); + } + + protected override RazorCodeDocument CreateCodeDocumentDesignTimeCore(RazorProjectItem projectItem) + { + if (projectItem == null) + { + throw new ArgumentNullException(nameof(projectItem)); + } + + var sourceDocument = RazorSourceDocument.ReadFrom(projectItem); + + var importFeature = GetRequiredFeature(); + var importItems = importFeature.GetImports(projectItem); + var importSourceDocuments = GetImportSourceDocuments(importItems); + + var parserOptions = GetRequiredFeature().Create(ConfigureDesignTimeParserOptions); + var codeGenerationOptions = GetRequiredFeature().Create(ConfigureDesignTimeCodeGenerationOptions); + + return RazorCodeDocument.Create(sourceDocument, importSourceDocuments, parserOptions, codeGenerationOptions); + } + + protected override void ProcessCore(RazorCodeDocument codeDocument) + { + if (codeDocument == null) + { + throw new ArgumentNullException(nameof(codeDocument)); + } Engine.Process(codeDocument); - - return codeDocument; } private TFeature GetRequiredFeature() where TFeature : IRazorProjectEngineFeature { - var feature = Features.OfType().FirstOrDefault(); + var feature = ProjectFeatures.OfType().FirstOrDefault(); if (feature == null) { throw new InvalidOperationException( @@ -76,5 +115,43 @@ namespace Microsoft.AspNetCore.Razor.Language return feature; } + + private void ConfigureParserOptions(RazorParserOptionsBuilder builder) + { + } + + private void ConfigureDesignTimeParserOptions(RazorParserOptionsBuilder builder) + { + builder.SetDesignTime(true); + } + + private void ConfigureCodeGenerationOptions(RazorCodeGenerationOptionsBuilder builder) + { + } + + private void ConfigureDesignTimeCodeGenerationOptions(RazorCodeGenerationOptionsBuilder builder) + { + builder.SetDesignTime(true); + builder.SuppressChecksum = true; + builder.SuppressMetadataAttributes = true; + } + + // Internal for testing + internal static IReadOnlyList GetImportSourceDocuments(IReadOnlyList importItems) + { + var imports = new List(); + for (var i = 0; i < importItems.Count; i++) + { + var importItem = importItems[i]; + + if (importItem.Exists) + { + var sourceDocument = RazorSourceDocument.ReadFrom(importItem); + imports.Add(sourceDocument); + } + } + + return imports; + } } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorProjectEngineBuilder.cs b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorProjectEngineBuilder.cs index 85af0c68f2..a07f4581ff 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorProjectEngineBuilder.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorProjectEngineBuilder.cs @@ -22,29 +22,19 @@ namespace Microsoft.AspNetCore.Razor.Language Phases = new List(); } + public override RazorConfiguration Configuration { get; } + public override RazorProjectFileSystem FileSystem { get; } public override ICollection Features { get; } public override IList Phases { get; } - - public override RazorConfiguration Configuration { get; } - + public override RazorProjectEngine Build() { - RazorEngine engine = null; - - if (Configuration.DesignTime) - { - engine = RazorEngine.CreateDesignTimeEmpty(ConfigureRazorEngine); - } - else - { - engine = RazorEngine.CreateEmpty(ConfigureRazorEngine); - } - - var projectEngineFeatures = Features.OfType().ToArray(); - var projectEngine = new DefaultRazorProjectEngine(engine, FileSystem, projectEngineFeatures); + var engine = RazorEngine.CreateEmpty(ConfigureRazorEngine); + var projectFeatures = Features.OfType().ToArray(); + var projectEngine = new DefaultRazorProjectEngine(Configuration, engine, FileSystem, projectFeatures); return projectEngine; } diff --git a/src/Microsoft.AspNetCore.Razor.Language/EmptyProjectFileSystem.cs b/src/Microsoft.AspNetCore.Razor.Language/EmptyProjectFileSystem.cs new file mode 100644 index 0000000000..61ced1271b --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Language/EmptyProjectFileSystem.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.AspNetCore.Razor.Language +{ + internal class EmptyProjectFileSystem : RazorProjectFileSystem + { + public override IEnumerable EnumerateItems(string basePath) + { + NormalizeAndEnsureValidPath(basePath); + return Enumerable.Empty(); + } + + public override RazorProjectItem GetItem(string path) + { + NormalizeAndEnsureValidPath(path); + return new NotFoundProjectItem(string.Empty, path); + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Language/Extensions/DefaultTagHelperTargetExtension.cs b/src/Microsoft.AspNetCore.Razor.Language/Extensions/DefaultTagHelperTargetExtension.cs index eacdf6b677..ad85ebf807 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Extensions/DefaultTagHelperTargetExtension.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Extensions/DefaultTagHelperTargetExtension.cs @@ -16,8 +16,6 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions { private static readonly string[] PrivateModifiers = new string[] { "private" }; - public bool DesignTime { get; set; } - public string RunnerVariableName { get; set; } = "__tagHelperRunner"; public string StringValueBufferVariableName { get; set; } = "__tagHelperStringValueBuffer"; @@ -82,7 +80,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions throw new InvalidOperationException(message); } - if (DesignTime) + if (context.Options.DesignTime) { context.RenderChildren(node); } @@ -136,7 +134,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions .Write(CreateTagHelperMethodName) .WriteLine("();"); - if (!DesignTime) + if (!context.Options.DesignTime) { context.CodeWriter.WriteInstanceMethodInvocation( ExecutionContextVariableName, @@ -153,7 +151,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions throw new InvalidOperationException(message); } - if (!DesignTime) + if (!context.Options.DesignTime) { context.CodeWriter .Write("await ") @@ -200,7 +198,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions throw new InvalidOperationException(message); } - if (DesignTime) + if (context.Options.DesignTime) { context.RenderChildren(node); } @@ -284,7 +282,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions throw new InvalidOperationException(message); } - if (!DesignTime) + if (!context.Options.DesignTime) { // Ensure that the property we're trying to set has initialized its dictionary bound properties. if (node.IsIndexerNameMatch && @@ -338,7 +336,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions // If we get there, this is the first time seeing this property so we need to evaluate the expression. if (node.BoundAttribute.ExpectsStringValue(node.AttributeName)) { - if (DesignTime) + if (context.Options.DesignTime) { context.RenderChildren(node); @@ -370,7 +368,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions } else { - if (DesignTime) + if (context.Options.DesignTime) { var firstMappedChild = node.Children.FirstOrDefault(child => child.Source != null) as IntermediateNode; var valueStart = firstMappedChild?.Source; @@ -456,7 +454,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions } } - if (!DesignTime) + if (!context.Options.DesignTime) { // We need to inform the context of the attribute value. context.CodeWriter @@ -474,7 +472,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions public void WriteTagHelperRuntime(CodeRenderingContext context, DefaultTagHelperRuntimeIntermediateNode node) { - if (!DesignTime) + if (!context.Options.DesignTime) { context.CodeWriter.WriteLine("#line hidden"); @@ -566,7 +564,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions } else if (node is IntermediateToken token) { - if (DesignTime && node.Source != null) + if (context.Options.DesignTime && node.Source != null) { context.AddSourceMappingFor(node); } diff --git a/src/Microsoft.AspNetCore.Razor.Language/Extensions/DesignTimeDirectivePass.cs b/src/Microsoft.AspNetCore.Razor.Language/Extensions/DesignTimeDirectivePass.cs index 31e9cbf361..6d6d7e261d 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Extensions/DesignTimeDirectivePass.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Extensions/DesignTimeDirectivePass.cs @@ -15,6 +15,13 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) { + // Only supports design time. This pass rewrites directives so they will have the right design time + // behavior and would break things if it ran for runtime. + if (!documentNode.Options.DesignTime) + { + return; + } + var walker = new DesignTimeHelperWalker(); walker.VisitDocument(documentNode); } diff --git a/src/Microsoft.AspNetCore.Razor.Language/Extensions/PreallocatedTagHelperAttributeOptimizationPass.cs b/src/Microsoft.AspNetCore.Razor.Language/Extensions/PreallocatedTagHelperAttributeOptimizationPass.cs index ed9488da20..f6d0f40a3b 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Extensions/PreallocatedTagHelperAttributeOptimizationPass.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Extensions/PreallocatedTagHelperAttributeOptimizationPass.cs @@ -15,6 +15,12 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) { + // There's no value in executing this pass at design time, it just prevents some allocations. + if (documentNode.Options.DesignTime) + { + return; + } + var walker = new PreallocatedTagHelperWalker(); walker.VisitDocument(documentNode); } diff --git a/src/Microsoft.AspNetCore.Razor.Language/IRazorImportFeature.cs b/src/Microsoft.AspNetCore.Razor.Language/IImportProjectFeature.cs similarity index 63% rename from src/Microsoft.AspNetCore.Razor.Language/IRazorImportFeature.cs rename to src/Microsoft.AspNetCore.Razor.Language/IImportProjectFeature.cs index f002cd20ce..98d8ffe558 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/IRazorImportFeature.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/IImportProjectFeature.cs @@ -5,8 +5,8 @@ using System.Collections.Generic; namespace Microsoft.AspNetCore.Razor.Language { - public interface IRazorImportFeature : IRazorProjectEngineFeature + public interface IImportProjectFeature : IRazorProjectEngineFeature { - IReadOnlyList GetImports(RazorProjectItem projectItem); + IReadOnlyList GetImports(RazorProjectItem projectItem); } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/IRazorCodeGenerationOptionsFactoryProjectFeature.cs b/src/Microsoft.AspNetCore.Razor.Language/IRazorCodeGenerationOptionsFactoryProjectFeature.cs new file mode 100644 index 0000000000..6d3ed5156c --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Language/IRazorCodeGenerationOptionsFactoryProjectFeature.cs @@ -0,0 +1,12 @@ +// 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.AspNetCore.Razor.Language +{ + internal interface IRazorCodeGenerationOptionsFactoryProjectFeature : IRazorProjectEngineFeature + { + RazorCodeGenerationOptions Create(Action configure); + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Language/IRazorCodeGenerationOptionsFeature.cs b/src/Microsoft.AspNetCore.Razor.Language/IRazorCodeGenerationOptionsFeature.cs index dc97b6326c..b74bcc3d8d 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/IRazorCodeGenerationOptionsFeature.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/IRazorCodeGenerationOptionsFeature.cs @@ -1,8 +1,11 @@ // 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.AspNetCore.Razor.Language { + [Obsolete("In Razor 2.1 and newer, use RazorCodeDocument.GetCodeGenerationOptions().")] public interface IRazorCodeGenerationOptionsFeature : IRazorEngineFeature { RazorCodeGenerationOptions GetOptions(); diff --git a/src/Microsoft.AspNetCore.Razor.Language/IRazorParserOptionsFactoryProjectFeature.cs b/src/Microsoft.AspNetCore.Razor.Language/IRazorParserOptionsFactoryProjectFeature.cs new file mode 100644 index 0000000000..e73bf8babc --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Language/IRazorParserOptionsFactoryProjectFeature.cs @@ -0,0 +1,12 @@ +// 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.AspNetCore.Razor.Language +{ + internal interface IRazorParserOptionsFactoryProjectFeature : IRazorProjectEngineFeature + { + RazorParserOptions Create(Action configure); + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Language/IRazorParserOptionsFeature.cs b/src/Microsoft.AspNetCore.Razor.Language/IRazorParserOptionsFeature.cs index ec4b3f1ab1..b2c1e9ec3e 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/IRazorParserOptionsFeature.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/IRazorParserOptionsFeature.cs @@ -1,8 +1,11 @@ // 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.AspNetCore.Razor.Language { + [Obsolete("In Razor 2.1 and newer, use RazorCodeDocument.GetParserOptions().")] public interface IRazorParserOptionsFeature : IRazorEngineFeature { RazorParserOptions GetOptions(); diff --git a/src/Microsoft.AspNetCore.Razor.Language/Intermediate/MethodDeclarationIntermediateNode.cs b/src/Microsoft.AspNetCore.Razor.Language/Intermediate/MethodDeclarationIntermediateNode.cs index 85bdd7641e..e1a039c5e0 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Intermediate/MethodDeclarationIntermediateNode.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Intermediate/MethodDeclarationIntermediateNode.cs @@ -14,6 +14,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Intermediate public string MethodName { get; set; } + public IList Parameters { get; } = new List(); + public string ReturnType { get; set; } public override void Accept(IntermediateNodeVisitor visitor) diff --git a/src/Microsoft.AspNetCore.Razor.Language/Intermediate/MethodParameter.cs b/src/Microsoft.AspNetCore.Razor.Language/Intermediate/MethodParameter.cs new file mode 100644 index 0000000000..6838f73d7d --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Language/Intermediate/MethodParameter.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Razor.Language.Intermediate +{ + public sealed class MethodParameter + { + public IList Modifiers { get; } = new List(); + + public string TypeName { get; set; } + + public string ParameterName { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/HtmlMarkupParser.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/HtmlMarkupParser.cs index 46599d4100..dcd0254c29 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/HtmlMarkupParser.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/HtmlMarkupParser.cs @@ -611,8 +611,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { if (NextIs(HtmlSymbolType.CloseAngle)) { - // We're at the end of a comment. check the condition 2.3 to make sure the text ending is allowed. - isValidComment = !EndsWithSymbolsSequence(p, HtmlSymbolType.OpenAngle, HtmlSymbolType.Bang, HtmlSymbolType.DoubleHyphen); + // Check condition 2.3: We're at the end of a comment. Check to make sure the text ending is allowed. + isValidComment = !SymbolSequenceEndsWithItems(p, HtmlSymbolType.OpenAngle, HtmlSymbolType.Bang, HtmlSymbolType.DoubleHyphen); return true; } else if (NextIs(ns => IsDashSymbol(ns) && NextIs(HtmlSymbolType.CloseAngle))) @@ -642,17 +642,17 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy return isValidComment; } - private bool EndsWithSymbolsSequence(IEnumerable symbols, params HtmlSymbolType[] sequenceToMatchWith) + internal static bool SymbolSequenceEndsWithItems(IEnumerable sequence, params HtmlSymbolType[] items) { - int index = sequenceToMatchWith.Length; - foreach (var previousSymbol in symbols) + int index = items.Length; + foreach (var previousSymbol in sequence) { if (index == 0) { break; } - if (sequenceToMatchWith[--index] != previousSymbol.Type) + if (items[--index] != previousSymbol.Type) return false; } diff --git a/src/Microsoft.AspNetCore.Razor.Language/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Razor.Language/Properties/AssemblyInfo.cs index 02e2547355..7214d8baca 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Properties/AssemblyInfo.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Properties/AssemblyInfo.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Razor.Performance, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Razor.Extensions.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Razor.GenerateTool, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Microsoft.AspNetCore.Razor.Language/ProvideRazorExtensionInitializerAttribute.cs b/src/Microsoft.AspNetCore.Razor.Language/ProvideRazorExtensionInitializerAttribute.cs new file mode 100644 index 0000000000..374419a676 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Language/ProvideRazorExtensionInitializerAttribute.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; + +namespace Microsoft.AspNetCore.Razor.Language +{ + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)] + public class ProvideRazorExtensionInitializerAttribute : Attribute + { + public ProvideRazorExtensionInitializerAttribute(string extensionName, Type initializerType) + { + if (extensionName == null) + { + throw new ArgumentNullException(nameof(extensionName)); + } + + if (initializerType == null) + { + throw new ArgumentNullException(nameof(initializerType)); + } + + ExtensionName = extensionName; + InitializerType = initializerType; + } + + public string ExtensionName { get; } + + public Type InitializerType { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorCodeDocument.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorCodeDocument.cs index 6ee41d0be5..0d57df2815 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/RazorCodeDocument.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorCodeDocument.cs @@ -30,6 +30,23 @@ namespace Microsoft.AspNetCore.Razor.Language return new DefaultRazorCodeDocument(source, imports); } + public static RazorCodeDocument Create( + RazorSourceDocument source, + IEnumerable imports, + RazorParserOptions parserOptions, + RazorCodeGenerationOptions codeGenerationOptions) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + var codeDocument = new DefaultRazorCodeDocument(source, imports); + codeDocument.SetParserOptions(parserOptions); + codeDocument.SetCodeGenerationOptions(codeGenerationOptions); + return codeDocument; + } + public abstract IReadOnlyList Imports { get; } public abstract ItemCollection Items { get; } diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorCodeDocumentExtensions.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorCodeDocumentExtensions.cs index 314b83d176..85df33837a 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/RazorCodeDocumentExtensions.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorCodeDocumentExtensions.cs @@ -109,6 +109,46 @@ namespace Microsoft.AspNetCore.Razor.Language document.Items[typeof(RazorCSharpDocument)] = csharp; } + public static RazorParserOptions GetParserOptions(this RazorCodeDocument document) + { + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + return (RazorParserOptions)document.Items[typeof(RazorParserOptions)]; + } + + public static void SetParserOptions(this RazorCodeDocument document, RazorParserOptions parserOptions) + { + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + document.Items[typeof(RazorParserOptions)] = parserOptions; + } + + public static RazorCodeGenerationOptions GetCodeGenerationOptions(this RazorCodeDocument document) + { + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + return (RazorCodeGenerationOptions)document.Items[typeof(RazorCodeGenerationOptions)]; + } + + public static void SetCodeGenerationOptions(this RazorCodeDocument document, RazorCodeGenerationOptions codeGenerationOptions) + { + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + document.Items[typeof(RazorCodeGenerationOptions)] = codeGenerationOptions; + } + private class ImportSyntaxTreesHolder { public ImportSyntaxTreesHolder(IReadOnlyList syntaxTrees) diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorCodeGenerationOptionsBuilder.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorCodeGenerationOptionsBuilder.cs index 55e1934ee7..be4c9dff3d 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/RazorCodeGenerationOptionsBuilder.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorCodeGenerationOptionsBuilder.cs @@ -5,6 +5,8 @@ namespace Microsoft.AspNetCore.Razor.Language { public abstract class RazorCodeGenerationOptionsBuilder { + public virtual RazorConfiguration Configuration => null; + public abstract bool DesignTime { get; } public abstract int IndentSize { get; set; } @@ -41,5 +43,9 @@ namespace Microsoft.AspNetCore.Razor.Language public virtual bool SuppressMetadataAttributes { get; set; } public abstract RazorCodeGenerationOptions Build(); + + public virtual void SetDesignTime(bool designTime) + { + } } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorConfiguration.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorConfiguration.cs index 0f00751497..f68db61f22 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/RazorConfiguration.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorConfiguration.cs @@ -12,22 +12,12 @@ namespace Microsoft.AspNetCore.Razor.Language public static readonly RazorConfiguration Default = new RazorConfiguration( RazorLanguageVersion.Latest, "unnamed", - Array.Empty(), - designTime: false); - - // This is used only in some back-compat scenarios. We don't expose it because there's no - // use case for anyone else to use it. - internal static readonly RazorConfiguration DefaultDesignTime = new RazorConfiguration( - RazorLanguageVersion.Latest, - "unnamed", - Array.Empty(), - designTime: true); + Array.Empty()); public RazorConfiguration( RazorLanguageVersion languageVersion, string configurationName, - IEnumerable extensions, - bool designTime) + IEnumerable extensions) { if (languageVersion == null) { @@ -47,7 +37,6 @@ namespace Microsoft.AspNetCore.Razor.Language LanguageVersion = languageVersion; ConfigurationName = configurationName; Extensions = extensions.ToArray(); - DesignTime = designTime; } public string ConfigurationName { get; } @@ -55,7 +44,5 @@ namespace Microsoft.AspNetCore.Razor.Language public IReadOnlyList Extensions { get; } public RazorLanguageVersion LanguageVersion { get; } - - public bool DesignTime { get; } } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorEngine.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorEngine.cs index a48c31893a..13f2f6be73 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/RazorEngine.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorEngine.cs @@ -17,27 +17,27 @@ namespace Microsoft.AspNetCore.Razor.Language return Create(configure: null); } - public static RazorEngine Create(Action configure) => CreateCore(RazorConfiguration.Default, configure); + public static RazorEngine Create(Action configure) => CreateCore(RazorConfiguration.Default, false, configure); public static RazorEngine CreateDesignTime() { return CreateDesignTime(configure: null); } - public static RazorEngine CreateDesignTime(Action configure) => CreateCore(RazorConfiguration.DefaultDesignTime, configure); + public static RazorEngine CreateDesignTime(Action configure) => CreateCore(RazorConfiguration.Default, true, configure); // Internal since RazorEngine APIs are going to be obsolete. - internal static RazorEngine CreateCore(RazorConfiguration configuration, Action configure) + internal static RazorEngine CreateCore(RazorConfiguration configuration, bool designTime, Action configure) { if (configuration == null) { throw new ArgumentNullException(nameof(configuration)); } - var builder = new DefaultRazorEngineBuilder(configuration.DesignTime); + var builder = new DefaultRazorEngineBuilder(designTime); AddDefaults(builder); - if (configuration.DesignTime) + if (designTime) { AddDefaultDesignTimeFeatures(configuration, builder.Features); } @@ -152,7 +152,7 @@ namespace Microsoft.AspNetCore.Razor.Language var targetExtension = features.OfType().FirstOrDefault(); Debug.Assert(targetExtension != null); - targetExtension.TargetExtensions.Add(new DefaultTagHelperTargetExtension() { DesignTime = false }); + targetExtension.TargetExtensions.Add(new DefaultTagHelperTargetExtension()); targetExtension.TargetExtensions.Add(new PreallocatedAttributeTargetExtension()); } @@ -170,7 +170,7 @@ namespace Microsoft.AspNetCore.Razor.Language var targetExtension = features.OfType().FirstOrDefault(); Debug.Assert(targetExtension != null); - targetExtension.TargetExtensions.Add(new DefaultTagHelperTargetExtension() { DesignTime = true }); + targetExtension.TargetExtensions.Add(new DefaultTagHelperTargetExtension()); targetExtension.TargetExtensions.Add(new DesignTimeDirectiveTargetExtension()); } diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorExtensionInitializer.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorExtensionInitializer.cs new file mode 100644 index 0000000000..5117115af6 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorExtensionInitializer.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Razor.Language +{ + public abstract class RazorExtensionInitializer + { + public abstract void Initialize(RazorProjectEngineBuilder builder); + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorParserOptionsBuilder.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorParserOptionsBuilder.cs index 74b11fcc26..b036e3d58d 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/RazorParserOptionsBuilder.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorParserOptionsBuilder.cs @@ -1,21 +1,26 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Collections.Generic; namespace Microsoft.AspNetCore.Razor.Language { public abstract class RazorParserOptionsBuilder { + public virtual RazorConfiguration Configuration => null; + public abstract bool DesignTime { get; } public abstract ICollection Directives { get; } public abstract bool ParseLeadingDirectives { get; set; } - public virtual RazorLanguageVersion Version { get; } + public virtual RazorLanguageVersion LanguageVersion { get; } public abstract RazorParserOptions Build(); + + public virtual void SetDesignTime(bool designTime) + { + } } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorProjectEngine.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorProjectEngine.cs index 7f2c03e8e3..341be49296 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/RazorProjectEngine.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorProjectEngine.cs @@ -3,26 +3,61 @@ using System; using System.Collections.Generic; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language.Extensions; namespace Microsoft.AspNetCore.Razor.Language { public abstract class RazorProjectEngine { + public abstract RazorConfiguration Configuration { get; } + public abstract RazorProjectFileSystem FileSystem { get; } public abstract RazorEngine Engine { get; } - public abstract IReadOnlyList Features { get; } + public IReadOnlyList EngineFeatures => Engine.Features; - public abstract RazorCodeDocument Process(RazorProjectItem projectItem); + public IReadOnlyList Phases => Engine.Phases; - public static RazorProjectEngine Create(RazorProjectFileSystem fileSystem) => Create(fileSystem, configure: null); + public abstract IReadOnlyList ProjectFeatures { get; } - public static RazorProjectEngine Create(RazorProjectFileSystem fileSystem, Action configure) => Create(fileSystem, RazorConfiguration.Default, configure); + public virtual RazorCodeDocument Process(RazorProjectItem projectItem) + { + if (projectItem == null) + { + throw new ArgumentNullException(nameof(projectItem)); + } + + var codeDocument = CreateCodeDocumentCore(projectItem); + ProcessCore(codeDocument); + return codeDocument; + } + + public virtual RazorCodeDocument ProcessDesignTime(RazorProjectItem projectItem) + { + if (projectItem == null) + { + throw new ArgumentNullException(nameof(projectItem)); + } + + var codeDocument = CreateCodeDocumentDesignTimeCore(projectItem); + ProcessCore(codeDocument); + return codeDocument; + } + + protected abstract RazorCodeDocument CreateCodeDocumentCore(RazorProjectItem projectItem); + + protected abstract RazorCodeDocument CreateCodeDocumentDesignTimeCore(RazorProjectItem projectItem); + + protected abstract void ProcessCore(RazorCodeDocument codeDocument); + + public static RazorProjectEngine Create(RazorConfiguration configuration, RazorProjectFileSystem fileSystem) => Create(configuration, fileSystem, configure: null); public static RazorProjectEngine Create( - RazorProjectFileSystem fileSystem, RazorConfiguration configuration, + RazorProjectFileSystem fileSystem, Action configure) { if (fileSystem == null) @@ -37,54 +72,96 @@ namespace Microsoft.AspNetCore.Razor.Language var builder = new DefaultRazorProjectEngineBuilder(configuration, fileSystem); - AddDefaults(builder); + // The intialization order is somewhat important. + // + // Defaults -> Extensions -> Additional customization + // + // This allows extensions to rely on default features, and customizations to override choices made by + // extensions. + RazorEngine.AddDefaultPhases(builder.Phases); + AddDefaultsFeatures(builder.Features); - if (configuration.DesignTime) - { - AddDesignTimeDefaults(builder); - } - else - { - AddRuntimeDefaults(builder); - } + LoadExtensions(builder, configuration.Extensions); configure?.Invoke(builder); return builder.Build(); } - - private static void AddDefaults(RazorProjectEngineBuilder builder) + + private static void AddDefaultsFeatures(ICollection features) { - builder.Features.Add(new DefaultRazorImportFeature()); - } + features.Add(new DefaultImportProjectFeature()); - private static void AddDesignTimeDefaults(RazorProjectEngineBuilder builder) - { - var engineFeatures = new List(); - RazorEngine.AddDefaultFeatures(engineFeatures); - RazorEngine.AddDefaultDesignTimeFeatures(builder.Configuration, engineFeatures); + // General extensibility + features.Add(new DefaultRazorDirectiveFeature()); + features.Add(new DefaultMetadataIdentifierFeature()); - AddEngineFeaturesAndPhases(builder, engineFeatures); - } + // Options features + features.Add(new DefaultRazorParserOptionsFactoryProjectFeature()); + features.Add(new DefaultRazorCodeGenerationOptionsFactoryProjectFeature()); - private static void AddRuntimeDefaults(RazorProjectEngineBuilder builder) - { - var engineFeatures = new List(); - RazorEngine.AddDefaultFeatures(engineFeatures); - RazorEngine.AddDefaultRuntimeFeatures(builder.Configuration, engineFeatures); + // Legacy options features + // + // These features are obsolete as of 2.1. Our code will resolve this but not invoke them. + features.Add(new DefaultRazorParserOptionsFeature(designTime: false, version: RazorLanguageVersion.Version_2_0)); + features.Add(new DefaultRazorCodeGenerationOptionsFeature(designTime: false)); - AddEngineFeaturesAndPhases(builder, engineFeatures); - } + // Syntax Tree passes + features.Add(new DefaultDirectiveSyntaxTreePass()); + features.Add(new HtmlNodeOptimizationPass()); + features.Add(new PreallocatedTagHelperAttributeOptimizationPass()); - private static void AddEngineFeaturesAndPhases(RazorProjectEngineBuilder builder, IReadOnlyList engineFeatures) - { - for (var i = 0; i < engineFeatures.Count; i++) + // Intermediate Node Passes + features.Add(new DefaultDocumentClassifierPass()); + features.Add(new MetadataAttributePass()); + features.Add(new DesignTimeDirectivePass()); + features.Add(new DirectiveRemovalOptimizationPass()); + features.Add(new DefaultTagHelperOptimizationPass()); + + // Default Code Target Extensions + var targetExtensionFeature = new DefaultRazorTargetExtensionFeature(); + features.Add(targetExtensionFeature); + targetExtensionFeature.TargetExtensions.Add(new MetadataAttributeTargetExtension()); + targetExtensionFeature.TargetExtensions.Add(new DefaultTagHelperTargetExtension()); + targetExtensionFeature.TargetExtensions.Add(new PreallocatedAttributeTargetExtension()); + targetExtensionFeature.TargetExtensions.Add(new DesignTimeDirectiveTargetExtension()); + + // Default configuration + var configurationFeature = new DefaultDocumentClassifierPassFeature(); + features.Add(configurationFeature); + configurationFeature.ConfigureClass.Add((document, @class) => { - var engineFeature = engineFeatures[i]; - builder.Features.Add(engineFeature); - } + @class.ClassName = "Template"; + @class.Modifiers.Add("public"); + }); - RazorEngine.AddDefaultPhases(builder.Phases); + configurationFeature.ConfigureNamespace.Add((document, @namespace) => + { + @namespace.Content = "Razor"; + }); + + configurationFeature.ConfigureMethod.Add((document, method) => + { + method.MethodName = "ExecuteAsync"; + method.ReturnType = $"global::{typeof(Task).FullName}"; + + method.Modifiers.Add("public"); + method.Modifiers.Add("async"); + method.Modifiers.Add("override"); + }); + } + + private static void LoadExtensions(RazorProjectEngineBuilder builder, IReadOnlyList extensions) + { + for (var i = 0; i < extensions.Count; i++) + { + // For now we only handle AssemblyExtension - which is not user-constructable. We're keeping a tight + // lid on how things work until we add official support for extensibility everywhere. So, this is + // intentionally inflexible for the time being. + var extension = extensions[i] as AssemblyExtension; + var initializer = extension?.CreateInitializer(); + initializer?.Initialize(builder); + } } } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorProjectEngineBuilder.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorProjectEngineBuilder.cs index 3177c4b13d..9fe4356fa9 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/RazorProjectEngineBuilder.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorProjectEngineBuilder.cs @@ -7,14 +7,14 @@ namespace Microsoft.AspNetCore.Razor.Language { public abstract class RazorProjectEngineBuilder { + public abstract RazorConfiguration Configuration { get; } + public abstract RazorProjectFileSystem FileSystem { get; } public abstract ICollection Features { get; } public abstract IList Phases { get; } - public abstract RazorConfiguration Configuration { get; } - public abstract RazorProjectEngine Build(); } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorProjectEngineBuilderExtensions.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorProjectEngineBuilderExtensions.cs index 8bd17e0dd6..ae92e4089b 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/RazorProjectEngineBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorProjectEngineBuilderExtensions.cs @@ -2,14 +2,80 @@ // 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; using Microsoft.AspNetCore.Razor.Language.CodeGeneration; +using Microsoft.AspNetCore.Razor.Language.Intermediate; namespace Microsoft.AspNetCore.Razor.Language { public static class RazorProjectEngineBuilderExtensions { - public static void SetImportFeature(this RazorProjectEngineBuilder builder, IRazorImportFeature feature) + /// + /// Registers a class configuration delegate that gets invoked during code generation. + /// + /// The . + /// invoked to configure + /// during code generation. + /// The . + public static RazorProjectEngineBuilder ConfigureClass( + this RazorProjectEngineBuilder builder, + Action configureClass) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (configureClass == null) + { + throw new ArgumentNullException(nameof(configureClass)); + } + + var configurationFeature = GetDefaultDocumentClassifierPassFeature(builder); + configurationFeature.ConfigureClass.Add(configureClass); + return builder; + } + + /// + /// Sets the base type for generated types. + /// + /// The . + /// The name of the base type. + /// The . + public static RazorProjectEngineBuilder SetBaseType(this RazorProjectEngineBuilder builder, string baseType) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + var configurationFeature = GetDefaultDocumentClassifierPassFeature(builder); + configurationFeature.ConfigureClass.Add((document, @class) => @class.BaseType = baseType); + return builder; + } + + /// + /// Sets the namespace for generated types. + /// + /// The . + /// The name of the namespace. + /// The . + public static RazorProjectEngineBuilder SetNamespace(this RazorProjectEngineBuilder builder, string namespaceName) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + var configurationFeature = GetDefaultDocumentClassifierPassFeature(builder); + configurationFeature.ConfigureNamespace.Add((document, @namespace) => @namespace.Content = namespaceName); + return builder; + } + + public static void SetImportFeature(this RazorProjectEngineBuilder builder, IImportProjectFeature feature) { if (builder == null) { @@ -22,7 +88,7 @@ namespace Microsoft.AspNetCore.Razor.Language } // Remove any existing import features in favor of the new one we're given. - var existingFeatures = builder.Features.OfType().ToArray(); + var existingFeatures = builder.Features.OfType().ToArray(); foreach (var existingFeature in existingFeatures) { builder.Features.Remove(existingFeature); @@ -79,6 +145,27 @@ namespace Microsoft.AspNetCore.Razor.Language return builder; } + /// + /// Adds the provided s as imports to all project items processed + /// by the . + /// + /// The . + /// The collection of imports. + /// The . + public static RazorProjectEngineBuilder AddDefaultImports(this RazorProjectEngineBuilder builder, params string[] imports) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + var existingImportFeature = builder.Features.OfType().First(); + var testImportFeature = new AdditionalImportsProjectFeature(existingImportFeature, imports); + builder.SetImportFeature(testImportFeature); + + return builder; + } + private static IRazorDirectiveFeature GetDirectiveFeature(RazorProjectEngineBuilder builder) { var directiveFeature = builder.Features.OfType().FirstOrDefault(); @@ -102,5 +189,77 @@ namespace Microsoft.AspNetCore.Razor.Language return targetExtensionFeature; } + + private static DefaultDocumentClassifierPassFeature GetDefaultDocumentClassifierPassFeature(RazorProjectEngineBuilder builder) + { + var configurationFeature = builder.Features.OfType().FirstOrDefault(); + if (configurationFeature == null) + { + configurationFeature = new DefaultDocumentClassifierPassFeature(); + builder.Features.Add(configurationFeature); + } + + return configurationFeature; + } + + private class AdditionalImportsProjectFeature : RazorProjectEngineFeatureBase, IImportProjectFeature + { + private readonly IImportProjectFeature _existingImportFeature; + private readonly IEnumerable _imports; + + public override RazorProjectEngine ProjectEngine + { + get => base.ProjectEngine; + set + { + _existingImportFeature.ProjectEngine = value; + base.ProjectEngine = value; + } + } + + public AdditionalImportsProjectFeature(IImportProjectFeature existingImportFeature, params string[] imports) + { + _existingImportFeature = existingImportFeature; + _imports = imports.Select(import => new InMemoryProjectItem(import)); + } + + public IReadOnlyList GetImports(RazorProjectItem projectItem) + { + var imports = _existingImportFeature.GetImports(projectItem).ToList(); + imports.AddRange(_imports); + + return imports; + } + + private class InMemoryProjectItem : RazorProjectItem + { + private readonly byte[] _importBytes; + + public InMemoryProjectItem(string content) + { + if (string.IsNullOrEmpty(content)) + { + throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(content)); + } + + var preamble = Encoding.UTF8.GetPreamble(); + var contentBytes = Encoding.UTF8.GetBytes(content); + + _importBytes = new byte[preamble.Length + contentBytes.Length]; + preamble.CopyTo(_importBytes, 0); + contentBytes.CopyTo(_importBytes, preamble.Length); + } + + public override string BasePath => null; + + public override string FilePath => null; + + public override string PhysicalPath => null; + + public override bool Exists => true; + + public override Stream Read() => new MemoryStream(_importBytes); + } + } } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorProjectFileSystem.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorProjectFileSystem.cs index a95de255df..799c6bda65 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/RazorProjectFileSystem.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorProjectFileSystem.cs @@ -7,6 +7,8 @@ namespace Microsoft.AspNetCore.Razor.Language { public abstract class RazorProjectFileSystem : RazorProject { + internal static readonly RazorProjectFileSystem Empty = new EmptyProjectFileSystem(); + /// /// Create a Razor project file system based off of a root directory. /// diff --git a/src/Microsoft.AspNetCore.Razor.Tasks/RazorGenerate.cs b/src/Microsoft.AspNetCore.Razor.Tasks/RazorGenerate.cs index 7389725f94..0eea45d546 100644 --- a/src/Microsoft.AspNetCore.Razor.Tasks/RazorGenerate.cs +++ b/src/Microsoft.AspNetCore.Razor.Tasks/RazorGenerate.cs @@ -9,9 +9,28 @@ namespace Microsoft.AspNetCore.Razor.Tasks { public class RazorGenerate : DotNetToolTask { + private static readonly string[] SourceRequiredMetadata = new string[] + { + FullPath, + GeneratedOutput, + TargetPath, + }; + private const string GeneratedOutput = "GeneratedOutput"; private const string TargetPath = "TargetPath"; private const string FullPath = "FullPath"; + private const string Identity = "Identity"; + private const string AssemblyName = "AssemblyName"; + private const string AssemblyFilePath = "AssemblyFilePath"; + + [Required] + public string Version { get; set; } + + [Required] + public ITaskItem[] Configuration { get; set; } + + [Required] + public ITaskItem[] Extensions { get; set; } [Required] public ITaskItem[] Sources { get; set; } @@ -26,11 +45,28 @@ namespace Microsoft.AspNetCore.Razor.Tasks protected override bool ValidateParameters() { + if (Configuration.Length == 0) + { + Log.LogError("The project {0} must provide a value for {1}.", ProjectRoot, nameof(Configuration)); + return false; + } + for (var i = 0; i < Sources.Length; i++) { if (!EnsureRequiredMetadata(Sources[i], FullPath) || !EnsureRequiredMetadata(Sources[i], GeneratedOutput) || !EnsureRequiredMetadata(Sources[i], TargetPath)) + { + Log.LogError("The Razor source item '{0}' is missing a required metadata entry. Required metadata are: '{1}'", Sources[i], SourceRequiredMetadata); + return false; + } + } + + for (var i = 0; i < Extensions.Length; i++) + { + if (!EnsureRequiredMetadata(Extensions[i], Identity) || + !EnsureRequiredMetadata(Extensions[i], AssemblyName) || + !EnsureRequiredMetadata(Extensions[i], AssemblyFilePath)) { return false; } @@ -65,6 +101,21 @@ namespace Microsoft.AspNetCore.Razor.Tasks builder.AppendLine("-t"); builder.AppendLine(TagHelperManifest); + builder.AppendLine("-v"); + builder.AppendLine(Version); + + builder.AppendLine("-c"); + builder.AppendLine(Configuration[0].GetMetadata(Identity)); + + for (var i = 0; i < Extensions.Length; i++) + { + builder.AppendLine("-n"); + builder.AppendLine(Extensions[i].GetMetadata(Identity)); + + builder.AppendLine("-e"); + builder.AppendLine(Path.GetFullPath(Extensions[i].GetMetadata(AssemblyFilePath))); + } + return builder.ToString(); } diff --git a/src/Microsoft.AspNetCore.Razor.Tasks/RazorTagHelper.cs b/src/Microsoft.AspNetCore.Razor.Tasks/RazorTagHelper.cs index 4064163c34..99d523a2b7 100644 --- a/src/Microsoft.AspNetCore.Razor.Tasks/RazorTagHelper.cs +++ b/src/Microsoft.AspNetCore.Razor.Tasks/RazorTagHelper.cs @@ -10,6 +10,19 @@ namespace Microsoft.AspNetCore.Razor.Tasks { public class RazorTagHelper : DotNetToolTask { + private const string Identity = "Identity"; + private const string AssemblyName = "AssemblyName"; + private const string AssemblyFilePath = "AssemblyFilePath"; + + [Required] + public string Version { get; set; } + + [Required] + public ITaskItem[] Configuration { get; set; } + + [Required] + public ITaskItem[] Extensions { get; set; } + [Required] public string[] Assemblies { get; set; } @@ -51,6 +64,21 @@ namespace Microsoft.AspNetCore.Razor.Tasks builder.AppendLine("-p"); builder.AppendLine(ProjectRoot); + builder.AppendLine("-v"); + builder.AppendLine(Version); + + builder.AppendLine("-c"); + builder.AppendLine(Configuration[0].GetMetadata(Identity)); + + for (var i = 0; i < Extensions.Length; i++) + { + builder.AppendLine("-n"); + builder.AppendLine(Extensions[i].GetMetadata(Identity)); + + builder.AppendLine("-e"); + builder.AppendLine(Path.GetFullPath(Extensions[i].GetMetadata(AssemblyFilePath))); + } + return builder.ToString(); } } diff --git a/src/Microsoft.AspNetCore.Razor.Tools/Application.cs b/src/Microsoft.AspNetCore.Razor.Tools/Application.cs index 483ab8c64c..c9f569747a 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/Application.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/Application.cs @@ -6,15 +6,19 @@ using System.Collections.Generic; using System.IO; using System.Reflection; using System.Threading; +using Microsoft.CodeAnalysis; using Microsoft.Extensions.CommandLineUtils; namespace Microsoft.AspNetCore.Razor.Tools { internal class Application : CommandLineApplication { - public Application(CancellationToken cancellationToken) + public Application(CancellationToken cancellationToken, ExtensionAssemblyLoader loader, ExtensionDependencyChecker checker, Func assemblyReferenceProvider) { CancellationToken = cancellationToken; + Checker = checker; + Loader = loader; + AssemblyReferenceProvider = assemblyReferenceProvider; Name = "rzc"; FullName = "Microsoft ASP.NET Core Razor CLI tool"; @@ -31,6 +35,12 @@ namespace Microsoft.AspNetCore.Razor.Tools public CancellationToken CancellationToken { get; } + public ExtensionAssemblyLoader Loader { get; } + + public ExtensionDependencyChecker Checker { get; } + + public Func AssemblyReferenceProvider { get; } + public new int Execute(params string[] args) { try diff --git a/src/Microsoft.AspNetCore.Razor.Tools/CachingMetadataReference.cs b/src/Microsoft.AspNetCore.Razor.Tools/CachingMetadataReference.cs new file mode 100644 index 0000000000..0aeb381b21 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Tools/CachingMetadataReference.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + internal sealed class CachingMetadataReference : PortableExecutableReference + { + private static readonly MetadataCache _metadataCache = new MetadataCache(); + + public CachingMetadataReference(string fullPath, MetadataReferenceProperties properties) + : base(properties, fullPath) + { + } + + protected override DocumentationProvider CreateDocumentationProvider() + { + return DocumentationProvider.Default; + } + + protected override Metadata GetMetadataImpl() + { + return _metadataCache.GetMetadata(FilePath); + } + + protected override PortableExecutableReference WithPropertiesImpl(MetadataReferenceProperties properties) + { + return new CachingMetadataReference(FilePath, properties); + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Tools/CompilerHost.cs b/src/Microsoft.AspNetCore.Razor.Tools/CompilerHost.cs index c4b96df6b2..ce1b0791f2 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/CompilerHost.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/CompilerHost.cs @@ -1,10 +1,12 @@ // 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.Threading; +using Microsoft.CodeAnalysis; namespace Microsoft.AspNetCore.Razor.Tools { @@ -19,6 +21,25 @@ namespace Microsoft.AspNetCore.Razor.Tools private class DefaultCompilerHost : CompilerHost { + public DefaultCompilerHost() + { + // The loader needs to live for the lifetime of the server. + // + // This means that if a request tries to use a set of binaries that are inconsistent with what + // the server already has, then it will be rejected to try again on the client. + // + // We also check each set of extensions for missing depenencies individually, so that we can + // consistently reject a request that doesn't specify everything it needs. Otherwise the request + // could succeed sometimes if it relies on transient state. + Loader = new DefaultExtensionAssemblyLoader(Path.Combine(Path.GetTempPath(), "Razor-Server")); + + AssemblyReferenceProvider = (path, properties) => new CachingMetadataReference(path, properties); + } + + public Func AssemblyReferenceProvider { get; } + + public ExtensionAssemblyLoader Loader { get; } + public override ServerResponse Execute(ServerRequest request, CancellationToken cancellationToken) { if (!TryParseArguments(request, out var parsed)) @@ -28,28 +49,23 @@ namespace Microsoft.AspNetCore.Razor.Tools var exitCode = 0; var output = string.Empty; - var app = new Application(cancellationToken); var commandArgs = parsed.args.ToArray(); + var writer = ServerLogger.IsLoggingEnabled ? new StringWriter() : TextWriter.Null; + + var checker = new DefaultExtensionDependencyChecker(Loader, writer, writer); + var app = new Application(cancellationToken, Loader, checker, AssemblyReferenceProvider) + { + Out = writer, + Error = writer, + }; + + exitCode = app.Execute(commandArgs); + if (ServerLogger.IsLoggingEnabled) { - using (var writer = new StringWriter()) - { - app.Out = writer; - app.Error = writer; - exitCode = app.Execute(commandArgs); - output = writer.ToString(); - ServerLogger.Log(output); - } - } - else - { - using (var writer = new StreamWriter(Stream.Null)) - { - app.Out = writer; - app.Error = writer; - exitCode = app.Execute(commandArgs); - } + output = writer.ToString(); + ServerLogger.Log(output); } return new CompletedServerResponse(exitCode, utf8output: false, output: string.Empty); diff --git a/src/Microsoft.AspNetCore.Razor.Tools/CompositeRazorProjectFileSystem.cs b/src/Microsoft.AspNetCore.Razor.Tools/CompositeRazorProjectFileSystem.cs index 5102cf056b..63fad347ee 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/CompositeRazorProjectFileSystem.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/CompositeRazorProjectFileSystem.cs @@ -3,24 +3,25 @@ using System; using System.Collections.Generic; +using System.Linq; using Microsoft.AspNetCore.Razor.Language; namespace Microsoft.AspNetCore.Razor.Tools { internal class CompositeRazorProjectFileSystem : RazorProjectFileSystem { - public CompositeRazorProjectFileSystem(IReadOnlyList projects) + public CompositeRazorProjectFileSystem(IReadOnlyList fileSystems) { - Projects = projects ?? throw new ArgumentNullException(nameof(projects)); + FileSystems = fileSystems ?? throw new ArgumentNullException(nameof(fileSystems)); } - public IReadOnlyList Projects { get; } + public IReadOnlyList FileSystems { get; } public override IEnumerable EnumerateItems(string basePath) { - foreach (var project in Projects) + foreach (var fileSystem in FileSystems) { - foreach (var result in project.EnumerateItems(basePath)) + foreach (var result in fileSystem.EnumerateItems(basePath)) { yield return result; } @@ -30,9 +31,9 @@ namespace Microsoft.AspNetCore.Razor.Tools public override RazorProjectItem GetItem(string path) { RazorProjectItem razorProjectItem = null; - foreach (var project in Projects) + foreach (var fileSystem in FileSystems) { - razorProjectItem = project.GetItem(path); + razorProjectItem = fileSystem.GetItem(path); if (razorProjectItem != null && razorProjectItem.Exists) { return razorProjectItem; diff --git a/src/Microsoft.AspNetCore.Razor.Tools/ConcurrentLruCache.cs b/src/Microsoft.AspNetCore.Razor.Tools/ConcurrentLruCache.cs new file mode 100644 index 0000000000..0220615ec2 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Tools/ConcurrentLruCache.cs @@ -0,0 +1,207 @@ +// 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.Diagnostics; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + /// + /// Cache with a fixed size that evicts the least recently used members. + /// Thread-safe. + /// This was taken from https://github.com/dotnet/roslyn/blob/749c0ec135d7d080658dc1aa794d15229c3d10d2/src/Compilers/Core/Portable/InternalUtilities/ConcurrentLruCache.cs. + /// + internal class ConcurrentLruCache + { + private readonly int _capacity; + + private readonly Dictionary _cache; + private readonly LinkedList _nodeList; + // This is a naive course-grained lock, it can probably be optimized + private readonly object _lockObject = new object(); + + public ConcurrentLruCache(int capacity) + : this (capacity, EqualityComparer.Default) + { + } + + public ConcurrentLruCache(int capacity, IEqualityComparer comparer) + { + if (capacity <= 0) + { + throw new ArgumentOutOfRangeException(nameof(capacity)); + } + _capacity = capacity; + _cache = new Dictionary(capacity, comparer); + _nodeList = new LinkedList(); + } + + /// + /// Create cache from an array. The cache capacity will be the size + /// of the array. All elements of the array will be added to the + /// cache. If any duplicate keys are found in the array a + /// will be thrown. + /// + public ConcurrentLruCache(KeyValuePair[] array) + : this(array.Length) + { + foreach (var kvp in array) + { + UnsafeAdd(kvp.Key, kvp.Value); + } + } + + public int Count + { + get + { + lock (_lockObject) + { + return _cache.Count; + } + } + } + + public void Add(TKey key, TValue value) + { + lock (_lockObject) + { + UnsafeAdd(key, value); + } + } + + public TValue GetOrAdd(TKey key, TValue value) + { + lock (_lockObject) + { + if (UnsafeTryGetValue(key, out var result)) + { + return result; + } + else + { + UnsafeAdd(key, value); + return value; + } + } + } + + public bool TryGetValue(TKey key, out TValue value) + { + lock (_lockObject) + { + return UnsafeTryGetValue(key, out value); + } + } + + public bool Remove(TKey key) + { + lock (_lockObject) + { + return UnsafeRemove(key); + } + } + + /// + /// For testing. Very expensive. + /// + internal IEnumerable> TestingEnumerable + { + get + { + lock (_lockObject) + { + foreach (var key in _nodeList) + { + var kvp = new KeyValuePair(key, _cache[key].Value); + yield return kvp; + } + } + } + } + + /// + /// Doesn't lock. + /// + private bool UnsafeTryGetValue(TKey key, out TValue value) + { + if (_cache.TryGetValue(key, out var result)) + { + MoveNodeToTop(result.Node); + value = result.Value; + return true; + } + else + { + value = default(TValue); + return false; + } + } + + private void MoveNodeToTop(LinkedListNode node) + { + if (!object.ReferenceEquals(_nodeList.First, node)) + { + _nodeList.Remove(node); + _nodeList.AddFirst(node); + } + } + + /// + /// Expects non-empty cache. Does not lock. + /// + private void UnsafeEvictLastNode() + { + Debug.Assert(_capacity > 0); + var lastNode = _nodeList.Last; + _nodeList.Remove(lastNode); + _cache.Remove(lastNode.Value); + } + + private void UnsafeAddNodeToTop(TKey key, TValue value) + { + var node = new LinkedListNode(key); + _cache.Add(key, new CacheValue(value, node)); + _nodeList.AddFirst(node); + } + + /// + /// Doesn't lock. + /// + private void UnsafeAdd(TKey key, TValue value) + { + if (_cache.TryGetValue(key, out var result)) + { + throw new ArgumentException("Key already exists", nameof(key)); + } + else + { + if (_cache.Count == _capacity) + { + UnsafeEvictLastNode(); + } + UnsafeAddNodeToTop(key, value); + } + } + + private bool UnsafeRemove(TKey key) + { + _nodeList.Remove(key); + return _cache.Remove(key); + } + + private struct CacheValue + { + public CacheValue(TValue value, LinkedListNode node) + { + Value = value; + Node = node; + } + + public TValue Value { get; } + + public LinkedListNode Node { get; } + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Tools/DefaultExtensionAssemblyLoader.cs b/src/Microsoft.AspNetCore.Razor.Tools/DefaultExtensionAssemblyLoader.cs new file mode 100644 index 0000000000..05dc4a09eb --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Tools/DefaultExtensionAssemblyLoader.cs @@ -0,0 +1,241 @@ +// 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.Collections.Immutable; +using System.IO; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using System.Runtime.Loader; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + internal class DefaultExtensionAssemblyLoader : ExtensionAssemblyLoader + { + private readonly string _baseDirectory; + + private readonly object _lock = new object(); + private readonly Dictionary _loadedByPath; + private readonly Dictionary _loadedByIdentity; + private readonly Dictionary _identityCache; + private readonly Dictionary> _wellKnownAssemblies; + + private ShadowCopyManager _shadowCopyManager; + + public DefaultExtensionAssemblyLoader(string baseDirectory) + { + _baseDirectory = baseDirectory; + + _loadedByPath = new Dictionary(StringComparer.OrdinalIgnoreCase); + _loadedByIdentity = new Dictionary(); + _identityCache = new Dictionary(StringComparer.OrdinalIgnoreCase); + _wellKnownAssemblies = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + LoadContext = new ExtensionAssemblyLoadContext(AssemblyLoadContext.GetLoadContext(typeof(ExtensionAssemblyLoader).Assembly), this); + } + + protected AssemblyLoadContext LoadContext { get; } + + public override void AddAssemblyLocation(string filePath) + { + if (filePath == null) + { + throw new ArgumentNullException(nameof(filePath)); + } + + if (!Path.IsPathRooted(filePath)) + { + throw new ArgumentException(nameof(filePath)); + } + + var assemblyName = Path.GetFileNameWithoutExtension(filePath); + lock (_lock) + { + if (!_wellKnownAssemblies.TryGetValue(assemblyName, out var paths)) + { + paths = new List(); + _wellKnownAssemblies.Add(assemblyName, paths); + } + + if (!paths.Contains(filePath)) + { + paths.Add(filePath); + } + } + } + + public override Assembly Load(string assemblyName) + { + if (!AssemblyIdentity.TryParseDisplayName(assemblyName, out var identity)) + { + return null; + } + + lock (_lock) + { + // First, check if this loader already loaded the requested assembly: + if (_loadedByIdentity.TryGetValue(identity, out var assembly)) + { + return assembly; + } + + // Second, check if an assembly file of the same simple name was registered with the loader: + if (_wellKnownAssemblies.TryGetValue(identity.Name, out var paths)) + { + // Multiple assemblies of the same simple name but different identities might have been registered. + // Load the one that matches the requested identity (if any). + foreach (var path in paths) + { + var candidateIdentity = GetIdentity(path); + + if (identity.Equals(candidateIdentity)) + { + return LoadFromPathUnsafe(path, candidateIdentity); + } + } + } + + // We only support loading by name from 'well-known' paths. If you need to load something by + // name and you get here, then that means we don't know where to look. + return null; + } + } + + public override Assembly LoadFromPath(string filePath) + { + if (filePath == null) + { + throw new ArgumentNullException(nameof(filePath)); + } + + if (!Path.IsPathRooted(filePath)) + { + throw new ArgumentException(nameof(filePath)); + } + + lock (_lock) + { + return LoadFromPathUnsafe(filePath, identity: null); + } + } + + private Assembly LoadFromPathUnsafe(string filePath, AssemblyIdentity identity) + { + // If we've already loaded the assembly by path there should be nothing else to do, + // all of our data is up to date. + if (_loadedByPath.TryGetValue(filePath, out var entry)) + { + return entry.assembly; + } + + // If we've already loaded the assembly by identity, then we might has some updating + // to do. + identity = identity ?? GetIdentity(filePath); + if (identity != null && _loadedByIdentity.TryGetValue(identity, out var assembly)) + { + // An assembly file might be replaced by another file with a different identity. + // Last one wins. + _loadedByPath[filePath] = (assembly, identity); + return assembly; + } + + // Ok we don't have this cached. Let's actually try to load the assembly. + assembly = LoadFromPathUnsafeCore(CopyAssembly(filePath)); + + identity = identity ?? AssemblyIdentity.FromAssemblyDefinition(assembly); + + // It's possible an assembly was loaded by two different paths. Just use the original then. + if (_loadedByIdentity.TryGetValue(identity, out var duplicate)) + { + assembly = duplicate; + } + else + { + _loadedByIdentity.Add(identity, assembly); + } + + _loadedByPath[filePath] = (assembly, identity); + return assembly; + } + + private AssemblyIdentity GetIdentity(string filePath) + { + if (!_identityCache.TryGetValue(filePath, out var identity)) + { + identity = ReadAssemblyIdentity(filePath); + _identityCache.Add(filePath, identity); + } + + return identity; + } + + protected virtual string CopyAssembly(string filePath) + { + if (_baseDirectory == null) + { + // Don't shadow-copy when base directory is null. This means we're running as a CLI not + // a server. + return filePath; + } + + if (_shadowCopyManager == null) + { + _shadowCopyManager = new ShadowCopyManager(_baseDirectory); + } + + return _shadowCopyManager.AddAssembly(filePath); + } + + protected virtual Assembly LoadFromPathUnsafeCore(string filePath) + { + return LoadContext.LoadFromAssemblyPath(filePath); + } + + private static AssemblyIdentity ReadAssemblyIdentity(string filePath) + { + try + { + using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete)) + using (var reader = new PEReader(stream)) + { + var metadataReader = reader.GetMetadataReader(); + return metadataReader.GetAssemblyIdentity(); + } + } + catch + { + } + + return null; + } + + private class ExtensionAssemblyLoadContext : AssemblyLoadContext + { + private readonly AssemblyLoadContext _parent; + private readonly DefaultExtensionAssemblyLoader _loader; + + public ExtensionAssemblyLoadContext(AssemblyLoadContext parent, DefaultExtensionAssemblyLoader loader) + { + _parent = parent; + _loader = loader; + } + + protected override Assembly Load(AssemblyName assemblyName) + { + // Try to load from well-known paths. This will be called when loading a dependency of an extension. + var assembly = _loader.Load(assemblyName.ToString()); + if (assembly != null) + { + return assembly; + } + + // If we don't have an entry, then fall back to the default load context. This allows extensions + // to resolve assemblies that are provided by the host. + return _parent.LoadFromAssemblyName(assemblyName); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Tools/DefaultExtensionDependencyChecker.cs b/src/Microsoft.AspNetCore.Razor.Tools/DefaultExtensionDependencyChecker.cs new file mode 100644 index 0000000000..79d01afb9d --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Tools/DefaultExtensionDependencyChecker.cs @@ -0,0 +1,158 @@ +// 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.Reflection; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + internal class DefaultExtensionDependencyChecker : ExtensionDependencyChecker + { + // These are treated as prefixes. So `Microsoft.CodeAnalysis.Razor` would be assumed to work. + private static readonly string[] DefaultIgnoredAssemblies = new string[] + { + "mscorlib", + "netstandard", + "System", + "Microsoft.CodeAnalysis", + "Microsoft.AspNetCore.Razor.Language", + }; + + private readonly ExtensionAssemblyLoader _loader; + private readonly TextWriter _output; + private readonly TextWriter _error; + private readonly string[] _ignoredAssemblies; + + public DefaultExtensionDependencyChecker( + ExtensionAssemblyLoader loader, + TextWriter output, + TextWriter error, + string[] ignoredAssemblies = null) + { + _loader = loader; + _output = output; + _error = error; + _ignoredAssemblies = ignoredAssemblies ?? DefaultIgnoredAssemblies; + } + + public override bool Check(IEnumerable assmblyFilePaths) + { + try + { + return CheckCore(assmblyFilePaths); + } + catch (Exception ex) + { + _error.WriteLine("Exception performing Extension dependency check:"); + _error.WriteLine(ex.ToString()); + return false; + } + } + + private bool CheckCore(IEnumerable assemblyFilePaths) + { + var items = assemblyFilePaths.Select(a => ExtensionVerificationItem.Create(a)).ToArray(); + var assemblies = new HashSet(items.Select(i => i.Identity)); + + for (var i = 0; i < items.Length; i++) + { + var item = items[i]; + _output.WriteLine($"Verifying assembly at {item.FilePath}"); + + if (!Path.IsPathRooted(item.FilePath)) + { + _error.WriteLine($"The file path '{item.FilePath}' is not a rooted path. File paths must be absolute and fully-qualified."); + return false; + } + + foreach (var reference in item.References) + { + if (_ignoredAssemblies.Any(n => reference.Name.StartsWith(n))) + { + // This is on the allow list, keep going. + continue; + } + + if (assemblies.Contains(reference)) + { + // This was also provided as a dependency, keep going. + continue; + } + + // If we get here we can't resolve this assembly. This is an error. + _error.WriteLine($"Extension assembly '{item.Identity.Name}' depends on '{reference.ToString()} which is missing."); + return false; + } + } + + // Assuming we get this far, the set of assemblies we have is at least a coherent set (barring + // version conflicts). Register all of the paths with the loader so they can find each other by + // name. + for (var i = 0; i < items.Length; i++) + { + _loader.AddAssemblyLocation(items[i].FilePath); + } + + // Now try to load everything. This has the side effect of resolving all of these items + // in the loader's caches. + for (var i = 0; i < items.Length; i++) + { + var item = items[i]; + item.Assembly = _loader.LoadFromPath(item.FilePath); + } + + // Third, check that the MVIDs of the files on disk match the MVIDs of the loaded assemblies. + for (var i = 0; i < items.Length; i++) + { + var item = items[i]; + if (item.Mvid != item.Assembly.ManifestModule.ModuleVersionId) + { + _error.WriteLine($"Extension assembly '{item.Identity.Name}' at '{item.FilePath}' has a different ModuleVersionId than loaded assembly '{item.Assembly.FullName}'"); + return false; + } + } + + return true; + } + + private class ExtensionVerificationItem + { + public static ExtensionVerificationItem Create(string filePath) + { + using (var peReader = new PEReader(new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))) + { + var metadataReader = peReader.GetMetadataReader(); + var identity = metadataReader.GetAssemblyIdentity(); + var mvid = metadataReader.GetGuid(metadataReader.GetModuleDefinition().Mvid); + var references = metadataReader.GetReferencedAssembliesOrThrow(); + + return new ExtensionVerificationItem(filePath, identity, mvid, references.ToArray()); + } + } + + private ExtensionVerificationItem(string filePath, AssemblyIdentity identity, Guid mvid, AssemblyIdentity[] references) + { + FilePath = filePath; + Identity = identity; + Mvid = mvid; + References = references; + } + + public string FilePath { get; } + + public Assembly Assembly { get; set; } + + public AssemblyIdentity Identity { get; } + + public Guid Mvid { get; } + + public IReadOnlyList References { get; } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Tools/DefaultRequestDispatcher.cs b/src/Microsoft.AspNetCore.Razor.Tools/DefaultRequestDispatcher.cs index 26747a06b6..59d2bbc098 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/DefaultRequestDispatcher.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/DefaultRequestDispatcher.cs @@ -383,6 +383,8 @@ namespace Microsoft.AspNetCore.Razor.Tools ServerLogger.Log("End writing response."); reason = ConnectionResult.Reason.CompilationCompleted; + + _eventBus.CompilationCompleted(); } catch { diff --git a/src/Microsoft.AspNetCore.Razor.Tools/DiscoverCommand.cs b/src/Microsoft.AspNetCore.Razor.Tools/DiscoverCommand.cs index 2b3cee612e..c846ef3692 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/DiscoverCommand.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/DiscoverCommand.cs @@ -8,7 +8,6 @@ using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.Razor.Extensions; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor; @@ -26,6 +25,10 @@ namespace Microsoft.AspNetCore.Razor.Tools Assemblies = Argument("assemblies", "assemblies to search for tag helpers", multipleValues: true); TagHelperManifest = Option("-o", "output file", CommandOptionType.SingleValue); ProjectDirectory = Option("-p", "project root directory", CommandOptionType.SingleValue); + Version = Option("-v|--version", "Razor language version", CommandOptionType.SingleValue); + Configuration = Option("-c", "Razor configuration name", CommandOptionType.SingleValue); + ExtensionNames = Option("-n", "extension name", CommandOptionType.MultipleValue); + ExtensionFilePaths = Option("-e", "extension file path", CommandOptionType.MultipleValue); } public CommandArgument Assemblies { get; } @@ -34,6 +37,14 @@ namespace Microsoft.AspNetCore.Razor.Tools public CommandOption ProjectDirectory { get; } + public CommandOption Version { get; } + + public CommandOption Configuration { get; } + + public CommandOption ExtensionNames { get; } + + public CommandOption ExtensionFilePaths { get; } + protected override bool ValidateArguments() { if (string.IsNullOrEmpty(TagHelperManifest.Value())) @@ -53,12 +64,61 @@ namespace Microsoft.AspNetCore.Razor.Tools ProjectDirectory.Values.Add(Environment.CurrentDirectory); } + if (string.IsNullOrEmpty(Version.Value())) + { + Error.WriteLine($"{Version.ValueName} must be specified."); + return false; + } + else if (!RazorLanguageVersion.TryParse(Version.Value(), out _)) + { + Error.WriteLine($"{Version.ValueName} is not a valid language version."); + return false; + } + + if (string.IsNullOrEmpty(Configuration.Value())) + { + Error.WriteLine($"{Configuration.ValueName} must be specified."); + return false; + } + + if (ExtensionNames.Values.Count != ExtensionFilePaths.Values.Count) + { + Error.WriteLine($"{ExtensionNames.ValueName} and {ExtensionFilePaths.ValueName} should have the same number of values."); + } + + foreach (var filePath in ExtensionFilePaths.Values) + { + if (!Path.IsPathRooted(filePath)) + { + Error.WriteLine($"Extension file paths must be fully-qualified, absolute paths."); + return false; + } + } + + if (!Parent.Checker.Check(ExtensionFilePaths.Values)) + { + Error.WriteLine($"Extenions could not be loaded. See output for details."); + return false; + } + return true; } protected override Task ExecuteCoreAsync() { + // Loading all of the extensions should succeed as the dependency checker will have already + // loaded them. + var extensions = new RazorExtension[ExtensionNames.Values.Count]; + for (var i = 0; i < ExtensionNames.Values.Count; i++) + { + extensions[i] = new AssemblyExtension(ExtensionNames.Values[i], Parent.Loader.LoadFromPath(ExtensionFilePaths.Values[i])); + } + + var version = RazorLanguageVersion.Parse(Version.Value()); + var configuration = new RazorConfiguration(version, Configuration.Value(), extensions); + var result = ExecuteCore( + configuration: configuration, projectDirectory: ProjectDirectory.Value(), outputFilePath: TagHelperManifest.Value(), assemblies: Assemblies.Values.ToArray()); @@ -66,36 +126,31 @@ namespace Microsoft.AspNetCore.Razor.Tools return Task.FromResult(result); } - private int ExecuteCore(string projectDirectory, string outputFilePath, string[] assemblies) + private int ExecuteCore(RazorConfiguration configuration, string projectDirectory, string outputFilePath, string[] assemblies) { outputFilePath = Path.Combine(projectDirectory, outputFilePath); var metadataReferences = new MetadataReference[assemblies.Length]; for (var i = 0; i < assemblies.Length; i++) { - metadataReferences[i] = MetadataReference.CreateFromFile(assemblies[i]); + metadataReferences[i] = Parent.AssemblyReferenceProvider(assemblies[i], default(MetadataReferenceProperties)); } - var engine = RazorEngine.Create((b) => + var engine = RazorProjectEngine.Create(configuration, RazorProjectFileSystem.Empty, b => { - RazorExtensions.Register(b); - b.Features.Add(new DefaultMetadataReferenceFeature() { References = metadataReferences }); b.Features.Add(new CompilationTagHelperFeature()); - - // TagHelperDescriptorProviders (actually do tag helper discovery) b.Features.Add(new DefaultTagHelperDescriptorProvider()); - b.Features.Add(new ViewComponentTagHelperDescriptorProvider()); }); - var feature = engine.Features.OfType().Single(); + var feature = engine.Engine.Features.OfType().Single(); var tagHelpers = feature.GetDescriptors(); using (var stream = new MemoryStream()) { Serialize(stream, tagHelpers); - stream.Position = 0L; + stream.Position = 0; var newHash = Hash(stream); var existingHash = Hash(outputFilePath); @@ -103,7 +158,7 @@ namespace Microsoft.AspNetCore.Razor.Tools if (!HashesEqual(newHash, existingHash)) { stream.Position = 0; - using (var output = File.OpenWrite(outputFilePath)) + using (var output = File.Open(outputFilePath, FileMode.Create)) { stream.CopyTo(output); } diff --git a/src/Microsoft.AspNetCore.Razor.Tools/EventBus.cs b/src/Microsoft.AspNetCore.Razor.Tools/EventBus.cs index 958a637deb..dacd5c07e7 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/EventBus.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/EventBus.cs @@ -37,6 +37,13 @@ namespace Microsoft.AspNetCore.Razor.Tools public virtual void ConnectionCompleted(int count) { } + + /// + /// Called when a compilation is completed successfully and the response is written to the stream. + /// + public virtual void CompilationCompleted() + { + } /// /// Called when a bad client connection was detected and the server will be shutting down as a diff --git a/src/Microsoft.AspNetCore.Razor.Tools/ExtensionAssemblyLoader.cs b/src/Microsoft.AspNetCore.Razor.Tools/ExtensionAssemblyLoader.cs new file mode 100644 index 0000000000..071eec2f82 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Tools/ExtensionAssemblyLoader.cs @@ -0,0 +1,16 @@ +// Copyright(c) .NET Foundation.All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + internal abstract class ExtensionAssemblyLoader + { + public abstract void AddAssemblyLocation(string filePath); + + public abstract Assembly Load(string assemblyName); + + public abstract Assembly LoadFromPath(string filePath); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Tools/ExtensionDependencyChecker.cs b/src/Microsoft.AspNetCore.Razor.Tools/ExtensionDependencyChecker.cs new file mode 100644 index 0000000000..02fd86d9e8 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Tools/ExtensionDependencyChecker.cs @@ -0,0 +1,12 @@ +// 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; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + internal abstract class ExtensionDependencyChecker + { + public abstract bool Check(IEnumerable extensionFilePaths); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Tools/GenerateCommand.cs b/src/Microsoft.AspNetCore.Razor.Tools/GenerateCommand.cs index fc0a876940..d19c566b76 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/GenerateCommand.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/GenerateCommand.cs @@ -3,9 +3,9 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.Razor.Extensions; using Microsoft.AspNetCore.Razor.Language; using Microsoft.Extensions.CommandLineUtils; using Microsoft.VisualStudio.LanguageServices.Razor; @@ -13,13 +13,6 @@ using Newtonsoft.Json; namespace Microsoft.AspNetCore.Razor.Tools { - internal class Builder - { - public static Builder Make(CommandBase result) => null; - - public static Builder Make(T result) => null; - } - internal class GenerateCommand : CommandBase { public GenerateCommand(Application parent) @@ -30,6 +23,10 @@ namespace Microsoft.AspNetCore.Razor.Tools RelativePaths = Option("-r", "Relative path", CommandOptionType.MultipleValue); ProjectDirectory = Option("-p", "project root directory", CommandOptionType.SingleValue); TagHelperManifest = Option("-t", "tag helper manifest file", CommandOptionType.SingleValue); + Version = Option("-v|--version", "Razor language version", CommandOptionType.SingleValue); + Configuration = Option("-c", "Razor configuration name", CommandOptionType.SingleValue); + ExtensionNames = Option("-n", "extension name", CommandOptionType.MultipleValue); + ExtensionFilePaths = Option("-e", "extension file path", CommandOptionType.MultipleValue); } public CommandOption Sources { get; } @@ -42,9 +39,29 @@ namespace Microsoft.AspNetCore.Razor.Tools public CommandOption TagHelperManifest { get; } + public CommandOption Version { get; } + + public CommandOption Configuration { get; } + + public CommandOption ExtensionNames { get; } + + public CommandOption ExtensionFilePaths { get; } + protected override Task ExecuteCoreAsync() { + // Loading all of the extensions should succeed as the dependency checker will have already + // loaded them. + var extensions = new RazorExtension[ExtensionNames.Values.Count]; + for (var i = 0; i < ExtensionNames.Values.Count; i++) + { + extensions[i] = new AssemblyExtension(ExtensionNames.Values[i], Parent.Loader.LoadFromPath(ExtensionFilePaths.Values[i])); + } + + var version = RazorLanguageVersion.Parse(Version.Value()); + var configuration = new RazorConfiguration(version, Configuration.Value(), extensions); + var result = ExecuteCore( + configuration: configuration, projectDirectory: ProjectDirectory.Value(), tagHelperManifest: TagHelperManifest.Value(), sources: Sources.Values, @@ -77,10 +94,48 @@ namespace Microsoft.AspNetCore.Razor.Tools ProjectDirectory.Values.Add(Environment.CurrentDirectory); } + if (string.IsNullOrEmpty(Version.Value())) + { + Error.WriteLine($"{Version.ValueName} must be specified."); + return false; + } + else if (!RazorLanguageVersion.TryParse(Version.Value(), out _)) + { + Error.WriteLine($"{Version.ValueName} is not a valid language version."); + return false; + } + + if (string.IsNullOrEmpty(Configuration.Value())) + { + Error.WriteLine($"{Configuration.ValueName} must be specified."); + return false; + } + + if (ExtensionNames.Values.Count != ExtensionFilePaths.Values.Count) + { + Error.WriteLine($"{ExtensionNames.ValueName} and {ExtensionFilePaths.ValueName} should have the same number of values."); + } + + foreach (var filePath in ExtensionFilePaths.Values) + { + if (!Path.IsPathRooted(filePath)) + { + Error.WriteLine($"Extension file paths must be fully-qualified, absolute paths."); + return false; + } + } + + if (!Parent.Checker.Check(ExtensionFilePaths.Values)) + { + Error.WriteLine($"Extensions could not be loaded. See output for details."); + return false; + } + return true; } private int ExecuteCore( + RazorConfiguration configuration, string projectDirectory, string tagHelperManifest, List sources, @@ -90,26 +145,19 @@ namespace Microsoft.AspNetCore.Razor.Tools tagHelperManifest = Path.Combine(projectDirectory, tagHelperManifest); var tagHelpers = GetTagHelpers(tagHelperManifest); - - var engine = RazorEngine.Create(b => + var inputItems = GetInputItems(projectDirectory, sources, outputs, relativePaths); + var compositeFileSystem = new CompositeRazorProjectFileSystem(new[] + { + GetVirtualRazorProjectSystem(inputItems), + RazorProjectFileSystem.Create(projectDirectory), + }); + + var engine = RazorProjectEngine.Create(configuration, compositeFileSystem, b => { - RazorExtensions.Register(b); - b.Features.Add(new StaticTagHelperFeature() { TagHelpers = tagHelpers, }); }); - - var inputItems = GetInputItems(projectDirectory, sources, outputs, relativePaths); - var compositeProject = new CompositeRazorProjectFileSystem( - new[] - { - GetVirtualRazorProjectSystem(inputItems), - RazorProjectFileSystem.Create(projectDirectory), - }); - - var templateEngine = new MvcRazorTemplateEngine(engine, compositeProject); - - var results = GenerateCode(templateEngine, inputItems); + var results = GenerateCode(engine, inputItems); var success = true; @@ -180,14 +228,15 @@ namespace Microsoft.AspNetCore.Razor.Tools return items; } - private OutputItem[] GenerateCode(RazorTemplateEngine templateEngine, SourceItem[] inputs) + private OutputItem[] GenerateCode(RazorProjectEngine engine, SourceItem[] inputs) { var outputs = new OutputItem[inputs.Length]; - Parallel.For(0, outputs.Length, new ParallelOptions() { MaxDegreeOfParallelism = 4 }, i => + Parallel.For(0, outputs.Length, new ParallelOptions() { MaxDegreeOfParallelism = Debugger.IsAttached ? 1 : 4 }, i => { var inputItem = inputs[i]; - var csharpDocument = templateEngine.GenerateCode(inputItem.FilePath); + var codeDocument = engine.Process(engine.FileSystem.GetItem(inputItem.FilePath)); + var csharpDocument = codeDocument.GetCSharpDocument(); outputs[i] = new OutputItem(inputItem, csharpDocument); }); diff --git a/src/Microsoft.AspNetCore.Razor.Tools/MetadataCache.cs b/src/Microsoft.AspNetCore.Razor.Tools/MetadataCache.cs new file mode 100644 index 0000000000..b0f9fd025e --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Tools/MetadataCache.cs @@ -0,0 +1,86 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection.PortableExecutable; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + internal class MetadataCache + { + // Store 1000 entries -- arbitrary number + private const int CacheSize = 1000; + private readonly ConcurrentLruCache _metadataCache = + new ConcurrentLruCache(CacheSize, StringComparer.OrdinalIgnoreCase); + + // For testing purposes only. + internal ConcurrentLruCache Cache => _metadataCache; + + internal Metadata GetMetadata(string fullPath) + { + var timestamp = GetFileTimeStamp(fullPath); + + // Check if we have an entry in the dictionary. + if (_metadataCache.TryGetValue(fullPath, out var entry)) + { + if (timestamp.HasValue && timestamp.Value == entry.Timestamp) + { + // The file has not changed since we cached it. Return the cached entry. + return entry.Metadata; + } + else + { + // The file has changed recently. Remove the cache entry. + _metadataCache.Remove(fullPath); + } + } + + Metadata metadata; + using (var fileStream = File.OpenRead(fullPath)) + { + metadata = AssemblyMetadata.CreateFromStream(fileStream, PEStreamOptions.PrefetchMetadata); + } + + _metadataCache.GetOrAdd(fullPath, new MetadataCacheEntry(timestamp.Value, metadata)); + + return metadata; + } + + private static DateTime? GetFileTimeStamp(string fullPath) + { + try + { + Debug.Assert(Path.IsPathRooted(fullPath)); + + return File.GetLastWriteTimeUtc(fullPath); + } + catch (Exception e) + { + // There are several exceptions that can occur here: NotSupportedException or PathTooLongException + // for a bad path, UnauthorizedAccessException for access denied, etc. Rather than listing them all, + // just catch all exceptions and log. + ServerLogger.LogException(e, $"Error getting timestamp of file {fullPath}."); + + return null; + } + } + + internal struct MetadataCacheEntry + { + public MetadataCacheEntry(DateTime timestamp, Metadata metadata) + { + Debug.Assert(timestamp.Kind == DateTimeKind.Utc); + + Timestamp = timestamp; + Metadata = metadata; + } + + public DateTime Timestamp { get; } + + public Metadata Metadata { get; } + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Tools/MetadataReaderExtensions.cs b/src/Microsoft.AspNetCore.Razor.Tools/MetadataReaderExtensions.cs new file mode 100644 index 0000000000..da1bddb865 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Tools/MetadataReaderExtensions.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft. 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.Collections.Immutable; +using System.Reflection; +using System.Reflection.Metadata; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + internal static class MetadataReaderExtensions + { + internal static AssemblyIdentity GetAssemblyIdentity(this MetadataReader reader) + { + if (!reader.IsAssembly) + { + throw new BadImageFormatException(); + } + + var definition = reader.GetAssemblyDefinition(); + + return CreateAssemblyIdentity( + reader, + definition.Version, + definition.Flags, + definition.PublicKey, + definition.Name, + definition.Culture, + isReference: false); + } + + internal static AssemblyIdentity[] GetReferencedAssembliesOrThrow(this MetadataReader reader) + { + var references = new List(); + + foreach (var referenceHandle in reader.AssemblyReferences) + { + var reference = reader.GetAssemblyReference(referenceHandle); + references.Add(CreateAssemblyIdentity( + reader, + reference.Version, + reference.Flags, + reference.PublicKeyOrToken, + reference.Name, + reference.Culture, + isReference: true)); + } + + return references.ToArray(); + } + + private static AssemblyIdentity CreateAssemblyIdentity( + MetadataReader reader, + Version version, + AssemblyFlags flags, + BlobHandle publicKey, + StringHandle name, + StringHandle culture, + bool isReference) + { + var publicKeyOrToken = reader.GetBlobContent(publicKey); + bool hasPublicKey; + + if (isReference) + { + hasPublicKey = (flags & AssemblyFlags.PublicKey) != 0; + } + else + { + // Assembly definitions never contain a public key token, they only can have a full key or nothing, + // so the flag AssemblyFlags.PublicKey does not make sense for them and is ignored. + // See Ecma-335, Partition II Metadata, 22.2 "Assembly : 0x20". + // This also corresponds to the behavior of the native C# compiler and sn.exe tool. + hasPublicKey = !publicKeyOrToken.IsEmpty; + } + + if (publicKeyOrToken.IsEmpty) + { + publicKeyOrToken = default; + } + + return new AssemblyIdentity( + name: reader.GetString(name), + version: version, + cultureName: culture.IsNil ? null : reader.GetString(culture), + publicKeyOrToken: publicKeyOrToken, + hasPublicKey: hasPublicKey, + isRetargetable: (flags & AssemblyFlags.Retargetable) != 0, + contentType: (AssemblyContentType)((int)(flags & AssemblyFlags.ContentTypeMask) >> 9)); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Tools/Microsoft.AspNetCore.Razor.Tools.csproj b/src/Microsoft.AspNetCore.Razor.Tools/Microsoft.AspNetCore.Razor.Tools.csproj index fe2b7c52b9..6aae7e8d50 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/Microsoft.AspNetCore.Razor.Tools.csproj +++ b/src/Microsoft.AspNetCore.Razor.Tools/Microsoft.AspNetCore.Razor.Tools.csproj @@ -27,7 +27,7 @@ - + diff --git a/src/Microsoft.AspNetCore.Razor.Tools/Program.cs b/src/Microsoft.AspNetCore.Razor.Tools/Program.cs index 8a23dd91b9..27807cd23e 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/Program.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/Program.cs @@ -3,6 +3,7 @@ using System; using System.Threading; +using Microsoft.CodeAnalysis; namespace Microsoft.AspNetCore.Razor.Tools { @@ -15,7 +16,16 @@ namespace Microsoft.AspNetCore.Razor.Tools var cancel = new CancellationTokenSource(); Console.CancelKeyPress += (sender, e) => { cancel.Cancel(); }; - var application = new Application(cancel.Token); + // Prevent shadow copying. + var loader = new DefaultExtensionAssemblyLoader(baseDirectory: null); + var checker = new DefaultExtensionDependencyChecker(loader, Console.Out, Console.Error); + + var application = new Application( + cancel.Token, + loader, + checker, + (path, properties) => MetadataReference.CreateFromFile(path, properties)); + return application.Execute(args); } } diff --git a/src/Microsoft.AspNetCore.Razor.Tools/ServerCommand.cs b/src/Microsoft.AspNetCore.Razor.Tools/ServerCommand.cs index f44487ab25..65a70b51ed 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/ServerCommand.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/ServerCommand.cs @@ -57,6 +57,7 @@ namespace Microsoft.AspNetCore.Razor.Tools } var host = ConnectionHost.Create(Pipe.Value()); + var compilerHost = CompilerHost.Create(); ExecuteServerCore(host, compilerHost, Cancelled, eventBus: null, keepAlive: keepAlive); } diff --git a/src/Microsoft.AspNetCore.Razor.Tools/ServerProtocol/ServerRequest.cs b/src/Microsoft.AspNetCore.Razor.Tools/ServerProtocol/ServerRequest.cs index e1dd9308b0..34b6a39eb9 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/ServerProtocol/ServerRequest.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/ServerProtocol/ServerRequest.cs @@ -170,7 +170,7 @@ namespace Microsoft.AspNetCore.Razor.Tools /// /// Write a Request to the stream. /// - public async Task WriteAsync(Stream outStream, CancellationToken cancellationToken = default) + public async Task WriteAsync(Stream outStream, CancellationToken cancellationToken = default(CancellationToken)) { using (var memoryStream = new MemoryStream()) using (var writer = new BinaryWriter(memoryStream, Encoding.Unicode)) diff --git a/src/Microsoft.AspNetCore.Razor.Tools/ShadowCopyManager.cs b/src/Microsoft.AspNetCore.Razor.Tools/ShadowCopyManager.cs new file mode 100644 index 0000000000..317dc5bd74 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Tools/ShadowCopyManager.cs @@ -0,0 +1,169 @@ +// 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.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + // Note that this class has no thread-safety guarantees. The caller should use a lock + // if concurrency is required. + internal class ShadowCopyManager : IDisposable + { + // Note that this class uses the *existance* of the Mutex to lock a directory. + // + // Nothing in this code actually ever acquires the Mutex, we just try to see if it exists + // already. + private readonly Mutex _mutex; + + private int _counter; + + public ShadowCopyManager(string baseDirectory = null) + { + BaseDirectory = baseDirectory ?? Path.Combine(Path.GetTempPath(), "Razor", "ShadowCopy"); + + var guid = Guid.NewGuid().ToString("N").ToLowerInvariant(); + UniqueDirectory = Path.Combine(BaseDirectory, guid); + + _mutex = new Mutex(initiallyOwned: false, name: guid); + + Directory.CreateDirectory(UniqueDirectory); + } + + public string BaseDirectory { get; } + + public string UniqueDirectory { get; } + + public string AddAssembly(string filePath) + { + var assemblyDirectory = CreateUniqueDirectory(); + + var destination = Path.Combine(assemblyDirectory, Path.GetFileName(filePath)); + CopyFile(filePath, destination); + + var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(filePath); + var resourcesNameWithoutExtension = fileNameWithoutExtension + ".resources"; + var resourcesNameWithExtension = resourcesNameWithoutExtension + ".dll"; + + foreach (var directory in Directory.EnumerateDirectories(Path.GetDirectoryName(filePath))) + { + var directoryName = Path.GetFileName(directory); + + var resourcesPath = Path.Combine(directory, resourcesNameWithExtension); + if (File.Exists(resourcesPath)) + { + var resourcesShadowCopyPath = Path.Combine(assemblyDirectory, directoryName, resourcesNameWithExtension); + CopyFile(resourcesPath, resourcesShadowCopyPath); + } + + resourcesPath = Path.Combine(directory, resourcesNameWithoutExtension, resourcesNameWithExtension); + if (File.Exists(resourcesPath)) + { + var resourcesShadowCopyPath = Path.Combine(assemblyDirectory, directoryName, resourcesNameWithoutExtension, resourcesNameWithExtension); + CopyFile(resourcesPath, resourcesShadowCopyPath); + } + } + + return destination; + } + + public void Dispose() + { + _mutex.ReleaseMutex(); + } + + public Task PurgeUnusedDirectoriesAsync() + { + return Task.Run((Action)PurgeUnusedDirectories); + } + + private string CreateUniqueDirectory() + { + var id = _counter++; + + var directory = Path.Combine(UniqueDirectory, id.ToString()); + Directory.CreateDirectory(directory); + return directory; + } + + private void CopyFile(string originalPath, string shadowCopyPath) + { + var directory = Path.GetDirectoryName(shadowCopyPath); + Directory.CreateDirectory(directory); + + File.Copy(originalPath, shadowCopyPath); + + MakeWritable(new FileInfo(shadowCopyPath)); + } + + private void MakeWritable(string directoryPath) + { + var directory = new DirectoryInfo(directoryPath); + + foreach (var file in directory.EnumerateFiles(searchPattern: "*", searchOption: SearchOption.AllDirectories)) + { + MakeWritable(file); + } + } + + private void MakeWritable(FileInfo file) + { + try + { + if (file.IsReadOnly) + { + file.IsReadOnly = false; + } + } + catch + { + // There are many reasons this could fail. Ignore it and keep going. + } + } + + private void PurgeUnusedDirectories() + { + IEnumerable directories; + try + { + directories = Directory.EnumerateDirectories(BaseDirectory); + } + catch (DirectoryNotFoundException) + { + return; + } + + foreach (var directory in directories) + { + Mutex mutex = null; + try + { + // We only want to try deleting the directory if no-one else is currently using it. + // + // Note that the mutex name is the name of the directory. This is OK because we're using + // GUIDs as directory/mutex names. + if (!Mutex.TryOpenExisting(Path.GetFileName(directory).ToLowerInvariant(), out mutex)) + { + MakeWritable(directory); + Directory.Delete(directory, recursive: true); + } + } + catch + { + // If something goes wrong we will leave it to the next run to clean up. + // Just swallow the exception and move on. + } + finally + { + if (mutex != null) + { + mutex.Dispose(); + } + } + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Tools/ShutdownCommand.cs b/src/Microsoft.AspNetCore.Razor.Tools/ShutdownCommand.cs index f77b30f5b2..3e0907774b 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/ShutdownCommand.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/ShutdownCommand.cs @@ -57,19 +57,21 @@ namespace Microsoft.AspNetCore.Razor.Tools var process = Process.GetProcessById(response.ServerProcessId); process.WaitForExit(); } - catch (Exception) + catch (Exception ex) { // There is an inherent race here with the server process. If it has already shutdown - // by the time we try to access it then the operation has succeed. + // by the time we try to access it then the operation has succeeded. + Error.Write(ex); } Out.Write("Server pid:{0} shut down", response.ServerProcessId); } } } - catch (Exception) when (IsServerRunning()) + catch (Exception ex) when (IsServerRunning()) { // Ignore an exception that occurred while the server was shutting down. + Error.Write(ex); } return 0; diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ExportCustomProjectEngineFactoryAttribute.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ExportCustomProjectEngineFactoryAttribute.cs new file mode 100644 index 0000000000..923cb0a66e --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ExportCustomProjectEngineFactoryAttribute.cs @@ -0,0 +1,28 @@ +// 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.Composition; + +namespace Microsoft.CodeAnalysis.Razor +{ + [MetadataAttribute] + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public class ExportCustomProjectEngineFactoryAttribute : ExportAttribute, ICustomProjectEngineFactoryMetadata + { + public ExportCustomProjectEngineFactoryAttribute(string configurationName) + : base(typeof(IProjectEngineFactory)) + { + if (configurationName == null) + { + throw new ArgumentNullException(nameof(configurationName)); + } + + ConfigurationName = configurationName; + } + + public string ConfigurationName { get; } + + public bool SupportsSerialization { get; set; } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ICustomProjectEngineFactoryMetadata.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ICustomProjectEngineFactoryMetadata.cs new file mode 100644 index 0000000000..3f93a6e605 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ICustomProjectEngineFactoryMetadata.cs @@ -0,0 +1,12 @@ +// 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.CodeAnalysis.Razor +{ + public interface ICustomProjectEngineFactoryMetadata + { + string ConfigurationName { get; } + + bool SupportsSerialization { get; } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/IProjectEngineFactory.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/IProjectEngineFactory.cs new file mode 100644 index 0000000000..903c47842b --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/IProjectEngineFactory.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.CodeAnalysis.Razor +{ + public interface IProjectEngineFactory + { + RazorProjectEngine Create(RazorConfiguration configuration, RazorProjectFileSystem fileSystem, Action configure); + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorTemplateEngineFactoryService.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorProjectEngineFactoryService.cs similarity index 61% rename from src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorTemplateEngineFactoryService.cs rename to src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorProjectEngineFactoryService.cs index 26ff5b803a..0388de4671 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorTemplateEngineFactoryService.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorProjectEngineFactoryService.cs @@ -7,8 +7,8 @@ using Microsoft.CodeAnalysis.Host; namespace Microsoft.CodeAnalysis.Razor { - internal abstract class RazorTemplateEngineFactoryService : ILanguageService + internal abstract class RazorProjectEngineFactoryService : ILanguageService { - public abstract RazorTemplateEngine Create(string projectPath, Action configure); + public abstract RazorProjectEngine Create(string projectPath, Action configure); } } \ No newline at end of file diff --git a/src/Microsoft.NET.Sdk.Razor/Sdk/Sdk.props b/src/Microsoft.NET.Sdk.Razor/Sdk/Sdk.props index a02a52aed3..9102f6c805 100644 --- a/src/Microsoft.NET.Sdk.Razor/Sdk/Sdk.props +++ b/src/Microsoft.NET.Sdk.Razor/Sdk/Sdk.props @@ -10,7 +10,16 @@ Copyright (c) .NET Foundation. All rights reserved. *********************************************************************************************** --> + + + <_RazorSdkImportsMicrosoftNetSdk Condition="'$(UsingMicrosoftNETSdk)' != 'true'">true + - + + + $(MSBuildThisFileDirectory)..\build\netstandard2.0\Sdk.Razor.CurrentVersion.props + + + diff --git a/src/Microsoft.NET.Sdk.Razor/Sdk/Sdk.targets b/src/Microsoft.NET.Sdk.Razor/Sdk/Sdk.targets index 82e5ccf1e8..20b70fd9ab 100644 --- a/src/Microsoft.NET.Sdk.Razor/Sdk/Sdk.targets +++ b/src/Microsoft.NET.Sdk.Razor/Sdk/Sdk.targets @@ -11,11 +11,13 @@ Copyright (c) .NET Foundation. All rights reserved. --> - - $(MSBuildThisFileDirectory)..\buildMultiTargeting\Sdk.Razor.CurrentVersion.MultiTargeting.targets - $(MSBuildThisFileDirectory)..\build\netstandard2.0\Sdk.Razor.CurrentVersion.targets + + + + $(MSBuildThisFileDirectory)..\buildMultiTargeting\Sdk.Razor.CurrentVersion.MultiTargeting.targets + $(MSBuildThisFileDirectory)..\build\netstandard2.0\Sdk.Razor.CurrentVersion.targets - + diff --git a/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Microsoft.NET.Sdk.Razor.DesignTime.targets b/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Microsoft.NET.Sdk.Razor.DesignTime.targets new file mode 100644 index 0000000000..0b012be77c --- /dev/null +++ b/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Microsoft.NET.Sdk.Razor.DesignTime.targets @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + File + + + File + + + Project + + + + diff --git a/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Microsoft.NET.Sdk.Razor.props b/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Microsoft.NET.Sdk.Razor.props index 439697f9bc..6fad4d5180 100644 --- a/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Microsoft.NET.Sdk.Razor.props +++ b/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Microsoft.NET.Sdk.Razor.props @@ -10,10 +10,8 @@ Copyright (c) .NET Foundation. All rights reserved. *********************************************************************************************** --> - - - - $(MSBuildThisFileDirectory)Sdk.Razor.CurrentVersion.targets + $(MSBuildThisFileDirectory)Sdk.Razor.CurrentVersion.props + $(MSBuildThisFileDirectory)Sdk.Razor.CurrentVersion.targets diff --git a/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Microsoft.NET.Sdk.Razor.targets b/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Microsoft.NET.Sdk.Razor.targets deleted file mode 100644 index 7a5540f394..0000000000 --- a/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Microsoft.NET.Sdk.Razor.targets +++ /dev/null @@ -1,14 +0,0 @@ - - - - diff --git a/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Sdk.Razor.CurrentVersion.props b/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Sdk.Razor.CurrentVersion.props index 8bd67bc624..c9778644e2 100644 --- a/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Sdk.Razor.CurrentVersion.props +++ b/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Sdk.Razor.CurrentVersion.props @@ -18,6 +18,12 @@ Copyright (c) .NET Foundation. All rights reserved. Default properties for common Razor SDK behavior. --> + + true + @@ -46,37 +52,24 @@ Copyright (c) .NET Foundation. All rights reserved. Implicit - false + false + + + true - - + + + $(IncludeRazorContentInPack) + + - - - - - - - - File - - - File - - - Project - - - diff --git a/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Sdk.Razor.CurrentVersion.targets b/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Sdk.Razor.CurrentVersion.targets index 2ea795e89b..fbad5a3ff9 100644 --- a/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Sdk.Razor.CurrentVersion.targets +++ b/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Sdk.Razor.CurrentVersion.targets @@ -34,13 +34,13 @@ Copyright (c) .NET Foundation. All rights reserved. ResolveRazorGenerateInputs; AssignRazorGenerateTargetPaths; ResolveAssemblyReferenceRazorGenerateInputs; - _EnsureRazorCompilerReferenced; + _CheckForMissingRazorCompiler; ResolveTagHelperRazorGenerateInputs PrepareForRazorGenerate; - _EnsureRazorCompilerReferenced; + _CheckForMissingRazorCompiler; RazorCoreGenerate @@ -50,20 +50,36 @@ Copyright (c) .NET Foundation. All rights reserved. - ResolveRazorEmbeddedResources + ResolveRazorEmbeddedResources PrepareForRazorCompile; RazorCoreCompile + + + $(BuiltProjectOutputGroupDependsOn); + _RazorAddBuiltProjectOutputGroupOutput + + + + $(DebugSymbolsProjectOutputGroupDependsOn); + _RazorAddDebugSymbolsProjectOutputGroupOutput + + + + _InitializePreserveCompilationContext; + $(PrepareForBuildDependsOn) + + - false + true @@ -83,9 +99,12 @@ Copyright (c) .NET Foundation. All rights reserved. $(IntermediateOutputPath)Razor\ + + + .Razor - $(TargetName).PrecompiledViews + $(TargetName)$(RazorTargetNameSuffix) - + PrecompilationTool @@ -154,6 +173,8 @@ Copyright (c) .NET Foundation. All rights reserved. <_RazorDebugSymbolsIntermediatePath Condition="'$(_RazorDebugSymbolsProduced)'=='true'" Include="$(IntermediateOutputPath)$(RazorTargetName).pdb" /> + + + + + true + + + - - $([MSBuild]::EnsureTrailingSlash('$(OutDir)')) - - - + + + + + + + + + @@ -393,18 +430,14 @@ Copyright (c) .NET Foundation. All rights reserved. --> - - $([MSBuild]::EnsureTrailingSlash('$(OutDir)')) - - - - - + + + + + $([MSBuild]::EnsureTrailingSlash('$(OutDir)')) + $([MSBuild]::Escape($([MSBuild]::EnsureTrailingSlash($([System.IO.Path]::GetFullPath('$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)', '$(RazorOutputPath)'))')))))) + + $(RazorTargetDir)$(RazorTargetName).dll + + + + + $(MSBuildExtensionsPath)\Microsoft\VisualStudio\Razor\Microsoft.NET.Sdk.Razor.DesignTime.targets + $(MSBuildThisFileDirectory)Microsoft.NET.Sdk.Razor.DesignTime.targets + + + + diff --git a/src/Microsoft.NET.Sdk.Razor/buildMultiTargeting/Microsoft.NET.Sdk.Razor.targets b/src/Microsoft.NET.Sdk.Razor/buildMultiTargeting/Microsoft.NET.Sdk.Razor.props similarity index 60% rename from src/Microsoft.NET.Sdk.Razor/buildMultiTargeting/Microsoft.NET.Sdk.Razor.targets rename to src/Microsoft.NET.Sdk.Razor/buildMultiTargeting/Microsoft.NET.Sdk.Razor.props index e1ef8742d3..a04423a57a 100644 --- a/src/Microsoft.NET.Sdk.Razor/buildMultiTargeting/Microsoft.NET.Sdk.Razor.targets +++ b/src/Microsoft.NET.Sdk.Razor/buildMultiTargeting/Microsoft.NET.Sdk.Razor.props @@ -1,6 +1,6 @@ - + + $(MSBuildThisFileDirectory)..\build\netstandard2.0\Sdk.Razor.CurrentVersion.props + $(MSBuildThisFileDirectory)Sdk.Razor.CurrentVersion.MultiTargeting.targets + diff --git a/src/Microsoft.VisualStudio.Editor.Razor/BackgroundParser.cs b/src/Microsoft.VisualStudio.Editor.Razor/BackgroundParser.cs index 887a98dab0..9a7cabb8e1 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/BackgroundParser.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/BackgroundParser.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using System.Text; using System.Threading; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Legacy; @@ -18,10 +19,10 @@ namespace Microsoft.VisualStudio.Editor.Razor private MainThreadState _main; private BackgroundThread _bg; - public BackgroundParser(RazorTemplateEngine templateEngine, string filePath) + public BackgroundParser(RazorProjectEngine projectEngine, string filePath, string projectDirectory) { _main = new MainThreadState(filePath); - _bg = new BackgroundThread(_main, templateEngine, filePath); + _bg = new BackgroundThread(_main, projectEngine, filePath, projectDirectory); _main.ResultsReady += (sender, args) => OnResultsReady(args); } @@ -233,22 +234,25 @@ namespace Microsoft.VisualStudio.Editor.Razor private class BackgroundThread : ThreadStateBase { + private readonly string _filePath; + private readonly string _relativeFilePath; + private readonly string _projectDirectory; private MainThreadState _main; private Thread _backgroundThread; private CancellationToken _shutdownToken; - private RazorTemplateEngine _templateEngine; - private string _filePath; + private RazorProjectEngine _projectEngine; private RazorSyntaxTree _currentSyntaxTree; private IList _previouslyDiscarded = new List(); - public BackgroundThread(MainThreadState main, RazorTemplateEngine templateEngine, string fileName) + public BackgroundThread(MainThreadState main, RazorProjectEngine projectEngine, string filePath, string projectDirectory) { // Run on MAIN thread! _main = main; _shutdownToken = _main.CancelToken; - _templateEngine = templateEngine; - _filePath = fileName; - + _projectEngine = projectEngine; + _filePath = filePath; + _relativeFilePath = GetNormalizedRelativeFilePath(filePath, projectDirectory); + _projectDirectory = projectDirectory; _backgroundThread = new Thread(WorkerLoop); SetThreadId(_backgroundThread.ManagedThreadId); } @@ -262,8 +266,6 @@ namespace Microsoft.VisualStudio.Editor.Razor // **** BACKGROUND THREAD **** private void WorkerLoop() { - var fileNameOnly = Path.GetFileName(_filePath); - try { EnsureOnThread(); @@ -347,14 +349,31 @@ namespace Microsoft.VisualStudio.Editor.Razor { EnsureOnThread(); - var sourceDocument = new TextSnapshotSourceDocument(snapshot, _filePath); - var imports = _templateEngine.GetImports(_filePath); + var projectItem = new TextSnapshotProjectItem(snapshot, _projectDirectory, _relativeFilePath, _filePath); + var codeDocument = _projectEngine.ProcessDesignTime(projectItem); - var codeDocument = RazorCodeDocument.Create(sourceDocument, imports); - - _templateEngine.GenerateCode(codeDocument); return codeDocument; } + + private string GetNormalizedRelativeFilePath(string filePath, string projectDirectory) + { + if (filePath.StartsWith(projectDirectory, StringComparison.OrdinalIgnoreCase)) + { + filePath = filePath.Substring(projectDirectory.Length); + } + + if (filePath.Length > 1) + { + filePath = filePath.Replace('\\', '/'); + + if (filePath[0] != '/') + { + filePath = "/" + filePath; + } + } + + return filePath; + } } private class WorkParcel diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultImportDocumentManager.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultImportDocumentManager.cs index 36f600a59a..2e8c5a450a 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultImportDocumentManager.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultImportDocumentManager.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Razor; @@ -15,7 +16,7 @@ namespace Microsoft.VisualStudio.Editor.Razor private readonly FileChangeTrackerFactory _fileChangeTrackerFactory; private readonly ForegroundDispatcher _foregroundDispatcher; private readonly ErrorReporter _errorReporter; - private readonly RazorTemplateEngineFactoryService _templateEngineFactoryService; + private readonly RazorProjectEngineFactoryService _projectEngineFactoryService; private readonly Dictionary _importTrackerCache; public override event EventHandler Changed; @@ -24,7 +25,7 @@ namespace Microsoft.VisualStudio.Editor.Razor ForegroundDispatcher foregroundDispatcher, ErrorReporter errorReporter, FileChangeTrackerFactory fileChangeTrackerFactory, - RazorTemplateEngineFactoryService templateEngineFactoryService) + RazorProjectEngineFactoryService projectEngineFactoryService) { if (foregroundDispatcher == null) { @@ -41,15 +42,15 @@ namespace Microsoft.VisualStudio.Editor.Razor throw new ArgumentNullException(nameof(fileChangeTrackerFactory)); } - if (templateEngineFactoryService == null) + if (projectEngineFactoryService == null) { - throw new ArgumentNullException(nameof(templateEngineFactoryService)); + throw new ArgumentNullException(nameof(projectEngineFactoryService)); } _foregroundDispatcher = foregroundDispatcher; _errorReporter = errorReporter; _fileChangeTrackerFactory = fileChangeTrackerFactory; - _templateEngineFactoryService = templateEngineFactoryService; + _projectEngineFactoryService = projectEngineFactoryService; _importTrackerCache = new Dictionary(StringComparer.OrdinalIgnoreCase); } @@ -115,10 +116,17 @@ namespace Microsoft.VisualStudio.Editor.Razor private IEnumerable GetImportItems(VisualStudioDocumentTracker tracker) { var projectDirectory = Path.GetDirectoryName(tracker.ProjectPath); - var templateEngine = _templateEngineFactoryService.Create(projectDirectory, _ => { }); - var imports = templateEngine.GetImportItems(tracker.FilePath); + var projectEngine = _projectEngineFactoryService.Create(projectDirectory, _ => { }); + var trackerItem = projectEngine.FileSystem.GetItem(tracker.FilePath); + var importFeature = projectEngine.ProjectFeatures.OfType().FirstOrDefault(); - return imports; + // There should always be an import feature unless someone has misconfigured their RazorProjectEngine. + // In that case once we attempt to parse the Razor file we'll explode and give the a user a decent + // error message; for now, lets just be extra protective and assume 0 imports to not give a bad error. + var importItems = importFeature?.GetImports(trackerItem) ?? Enumerable.Empty(); + var physicalImports = importItems.Where(import => import.FilePath != null); + + return physicalImports; } private void OnChanged(ImportTracker importTracker, FileChangeKind changeKind) diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultImportDocumentManagerFactory.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultImportDocumentManagerFactory.cs index 480374c4f8..47efa8b96c 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultImportDocumentManagerFactory.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultImportDocumentManagerFactory.cs @@ -35,13 +35,13 @@ namespace Microsoft.VisualStudio.Editor.Razor var errorReporter = languageServices.WorkspaceServices.GetRequiredService(); var fileChangeTrackerFactory = languageServices.GetRequiredService(); - var templateEngineFactoryService = languageServices.GetRequiredService(); + var projectEngineFactoryService = languageServices.GetRequiredService(); return new DefaultImportDocumentManager( _foregroundDispatcher, errorReporter, fileChangeTrackerFactory, - templateEngineFactoryService); + projectEngineFactoryService); } } } diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultTemplateEngineFactoryService.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryService.cs similarity index 76% rename from src/Microsoft.VisualStudio.Editor.Razor/DefaultTemplateEngineFactoryService.cs rename to src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryService.cs index 6c615077c7..5d1373729c 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultTemplateEngineFactoryService.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryService.cs @@ -12,7 +12,7 @@ using MvcLatest = Microsoft.AspNetCore.Mvc.Razor.Extensions; namespace Microsoft.VisualStudio.Editor.Razor { - internal class DefaultTemplateEngineFactoryService : RazorTemplateEngineFactoryService + internal class DefaultProjectEngineFactoryService : RazorProjectEngineFactoryService { private readonly static MvcExtensibilityConfiguration DefaultConfiguration = new MvcExtensibilityConfiguration( RazorLanguageVersion.Version_2_0, @@ -22,7 +22,7 @@ namespace Microsoft.VisualStudio.Editor.Razor private readonly ProjectSnapshotManager _projectManager; - public DefaultTemplateEngineFactoryService(ProjectSnapshotManager projectManager) + public DefaultProjectEngineFactoryService(ProjectSnapshotManager projectManager) { if (projectManager == null) { @@ -32,7 +32,7 @@ namespace Microsoft.VisualStudio.Editor.Razor _projectManager = projectManager; } - public override RazorTemplateEngine Create(string projectPath, Action configure) + public override RazorProjectEngine Create(string projectPath, Action configure) { if (projectPath == null) { @@ -43,12 +43,14 @@ namespace Microsoft.VisualStudio.Editor.Razor var project = FindProject(projectPath); var configuration = (project?.Configuration as MvcExtensibilityConfiguration) ?? DefaultConfiguration; var razorLanguageVersion = configuration.LanguageVersion; - var razorConfiguration = new RazorConfiguration(razorLanguageVersion, "unnamed", Array.Empty(), designTime: true); - RazorEngine engine; + var razorConfiguration = new RazorConfiguration(razorLanguageVersion, "unnamed", Array.Empty()); + var fileSystem = RazorProjectFileSystem.Create(projectPath); + + RazorProjectEngine projectEngine; if (razorLanguageVersion.Major == 1) { - engine = RazorEngine.CreateCore(razorConfiguration, b => + projectEngine = RazorProjectEngine.Create(razorConfiguration, fileSystem, b => { configure?.Invoke(b); @@ -59,24 +61,18 @@ namespace Microsoft.VisualStudio.Editor.Razor Mvc1_X.RazorExtensions.RegisterViewComponentTagHelpers(b); } }); - - var templateEngine = new Mvc1_X.MvcRazorTemplateEngine(engine, RazorProjectFileSystem.Create(projectPath)); - templateEngine.Options.ImportsFileName = "_ViewImports.cshtml"; - return templateEngine; } else { - engine = RazorEngine.CreateCore(razorConfiguration, b => + projectEngine = RazorProjectEngine.Create(razorConfiguration, fileSystem, b => { configure?.Invoke(b); MvcLatest.RazorExtensions.Register(b); }); - - var templateEngine = new MvcLatest.MvcRazorTemplateEngine(engine, RazorProjectFileSystem.Create(projectPath)); - templateEngine.Options.ImportsFileName = "_ViewImports.cshtml"; - return templateEngine; } + + return projectEngine; } private ProjectSnapshot FindProject(string directory) diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultTemplateEngineFactoryServiceFactory.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryServiceFactory.cs similarity index 60% rename from src/Microsoft.VisualStudio.Editor.Razor/DefaultTemplateEngineFactoryServiceFactory.cs rename to src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryServiceFactory.cs index f95df76c08..babed97a5c 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultTemplateEngineFactoryServiceFactory.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryServiceFactory.cs @@ -8,12 +8,12 @@ using Microsoft.CodeAnalysis.Razor.ProjectSystem; namespace Microsoft.VisualStudio.Editor.Razor { - [ExportLanguageServiceFactory(typeof(RazorTemplateEngineFactoryService), RazorLanguage.Name, ServiceLayer.Default)] - internal class DefaultTemplateEngineFactoryServiceFactory : ILanguageServiceFactory + [ExportLanguageServiceFactory(typeof(RazorProjectEngineFactoryService), RazorLanguage.Name, ServiceLayer.Default)] + internal class DefaultProjectEngineFactoryServiceFactory : ILanguageServiceFactory { public ILanguageService CreateLanguageService(HostLanguageServices languageServices) { - return new DefaultTemplateEngineFactoryService(languageServices.GetRequiredService()); + return new DefaultProjectEngineFactoryService(languageServices.GetRequiredService()); } } } \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParser.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParser.cs index 053b980e86..ddbd000f0b 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParser.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParser.cs @@ -31,9 +31,9 @@ namespace Microsoft.VisualStudio.Editor.Razor private readonly VisualStudioCompletionBroker _completionBroker; private readonly VisualStudioDocumentTracker _documentTracker; private readonly ForegroundDispatcher _dispatcher; - private readonly RazorTemplateEngineFactoryService _templateEngineFactory; + private readonly RazorProjectEngineFactoryService _projectEngineFactory; private readonly ErrorReporter _errorReporter; - private RazorTemplateEngine _templateEngine; + private RazorProjectEngine _projectEngine; private RazorCodeDocument _codeDocument; private ITextSnapshot _snapshot; private bool _disposed; @@ -47,7 +47,7 @@ namespace Microsoft.VisualStudio.Editor.Razor public DefaultVisualStudioRazorParser( ForegroundDispatcher dispatcher, VisualStudioDocumentTracker documentTracker, - RazorTemplateEngineFactoryService templateEngineFactory, + RazorProjectEngineFactoryService projectEngineFactory, ErrorReporter errorReporter, VisualStudioCompletionBroker completionBroker) { @@ -61,9 +61,9 @@ namespace Microsoft.VisualStudio.Editor.Razor throw new ArgumentNullException(nameof(documentTracker)); } - if (templateEngineFactory == null) + if (projectEngineFactory == null) { - throw new ArgumentNullException(nameof(templateEngineFactory)); + throw new ArgumentNullException(nameof(projectEngineFactory)); } if (errorReporter == null) @@ -77,7 +77,7 @@ namespace Microsoft.VisualStudio.Editor.Razor } _dispatcher = dispatcher; - _templateEngineFactory = templateEngineFactory; + _projectEngineFactory = projectEngineFactory; _errorReporter = errorReporter; _completionBroker = completionBroker; _documentTracker = documentTracker; @@ -85,8 +85,6 @@ namespace Microsoft.VisualStudio.Editor.Razor _documentTracker.ContextChanged += DocumentTracker_ContextChanged; } - public override RazorTemplateEngine TemplateEngine => _templateEngine; - public override string FilePath => _documentTracker.FilePath; public override RazorCodeDocument CodeDocument => _codeDocument; @@ -170,8 +168,8 @@ namespace Microsoft.VisualStudio.Editor.Razor _dispatcher.AssertForegroundThread(); var projectDirectory = Path.GetDirectoryName(_documentTracker.ProjectPath); - _templateEngine = _templateEngineFactory.Create(projectDirectory, ConfigureTemplateEngine); - _parser = new BackgroundParser(TemplateEngine, FilePath); + _projectEngine = _projectEngineFactory.Create(projectDirectory, ConfigureProjectEngine); + _parser = new BackgroundParser(_projectEngine, FilePath, projectDirectory); _parser.ResultsReady += OnResultsReady; _parser.Start(); @@ -383,7 +381,7 @@ namespace Microsoft.VisualStudio.Editor.Razor DocumentStructureChanged?.Invoke(this, args); } - private void ConfigureTemplateEngine(IRazorEngineBuilder builder) + private void ConfigureProjectEngine(RazorProjectEngineBuilder builder) { builder.Features.Add(new VisualStudioParserOptionsFeature(_documentTracker.EditorSettings)); builder.Features.Add(new VisualStudioTagHelperFeature(_documentTracker.TagHelpers)); diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParserFactory.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParserFactory.cs index 9e9ea12135..5318ede596 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParserFactory.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParserFactory.cs @@ -9,7 +9,7 @@ namespace Microsoft.VisualStudio.Editor.Razor internal class DefaultVisualStudioRazorParserFactory : VisualStudioRazorParserFactory { private readonly ForegroundDispatcher _dispatcher; - private readonly RazorTemplateEngineFactoryService _templateEngineFactoryService; + private readonly RazorProjectEngineFactoryService _projectEngineFactoryService; private readonly VisualStudioCompletionBroker _completionBroker; private readonly ErrorReporter _errorReporter; @@ -17,7 +17,7 @@ namespace Microsoft.VisualStudio.Editor.Razor ForegroundDispatcher dispatcher, ErrorReporter errorReporter, VisualStudioCompletionBroker completionBroker, - RazorTemplateEngineFactoryService templateEngineFactoryService) + RazorProjectEngineFactoryService projectEngineFactoryService) { if (dispatcher == null) { @@ -34,15 +34,15 @@ namespace Microsoft.VisualStudio.Editor.Razor throw new ArgumentNullException(nameof(completionBroker)); } - if (templateEngineFactoryService == null) + if (projectEngineFactoryService == null) { - throw new ArgumentNullException(nameof(templateEngineFactoryService)); + throw new ArgumentNullException(nameof(projectEngineFactoryService)); } _dispatcher = dispatcher; _errorReporter = errorReporter; _completionBroker = completionBroker; - _templateEngineFactoryService = templateEngineFactoryService; + _projectEngineFactoryService = projectEngineFactoryService; } public override VisualStudioRazorParser Create(VisualStudioDocumentTracker documentTracker) @@ -57,7 +57,7 @@ namespace Microsoft.VisualStudio.Editor.Razor var parser = new DefaultVisualStudioRazorParser( _dispatcher, documentTracker, - _templateEngineFactoryService, + _projectEngineFactoryService, _errorReporter, _completionBroker); return parser; diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParserFactoryFactory.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParserFactoryFactory.cs index d1f2d94cdc..38cfe5f189 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParserFactoryFactory.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParserFactoryFactory.cs @@ -35,13 +35,13 @@ namespace Microsoft.VisualStudio.Editor.Razor var workspaceServices = languageServices.WorkspaceServices; var errorReporter = workspaceServices.GetRequiredService(); var completionBroker = languageServices.GetRequiredService(); - var templateEngineFactoryService = languageServices.GetRequiredService(); + var projectEngineFactoryService = languageServices.GetRequiredService(); return new DefaultVisualStudioRazorParserFactory( _foregroundDispatcher, errorReporter, completionBroker, - templateEngineFactoryService); + projectEngineFactoryService); } } } \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.Editor.Razor/TextSnapshotProjectItem.cs b/src/Microsoft.VisualStudio.Editor.Razor/TextSnapshotProjectItem.cs new file mode 100644 index 0000000000..4a605fd4a4 --- /dev/null +++ b/src/Microsoft.VisualStudio.Editor.Razor/TextSnapshotProjectItem.cs @@ -0,0 +1,65 @@ +// 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 System.Text; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.VisualStudio.Text; + +namespace Microsoft.VisualStudio.Editor.Razor +{ + internal class TextSnapshotProjectItem : RazorProjectItem + { + private readonly ITextSnapshot _snapshot; + + public TextSnapshotProjectItem(ITextSnapshot snapshot, string projectDirectory, string relativeFilePath, string filePath) + { + if (snapshot == null) + { + throw new ArgumentNullException(nameof(snapshot)); + } + + if (string.IsNullOrEmpty(projectDirectory)) + { + throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(projectDirectory)); + } + + if (string.IsNullOrEmpty(relativeFilePath)) + { + throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(relativeFilePath)); + } + + if (string.IsNullOrEmpty(filePath)) + { + throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(filePath)); + } + + _snapshot = snapshot; + BasePath = projectDirectory; + FilePath = relativeFilePath; + PhysicalPath = filePath; + } + + public override string BasePath { get; } + + public override string FilePath { get; } + + public override string PhysicalPath { get; } + + public override bool Exists => true; + + public override Stream Read() + { + var charArray = _snapshot.ToCharArray(0, _snapshot.Length); + + // We can assume UTF8 because the call path that reads from RazorProjectItem => SourceDocument + // can't determine the encoding and always assumes Encoding.UTF8. This is something that we might + // want to revisit in the future. + var bytes = Encoding.UTF8.GetBytes(charArray); + var memoryStream = new MemoryStream(bytes); + return memoryStream; + } + } + +} diff --git a/src/Microsoft.VisualStudio.Editor.Razor/TextSnapshotSourceDocument.cs b/src/Microsoft.VisualStudio.Editor.Razor/TextSnapshotSourceDocument.cs deleted file mode 100644 index ffe6c2f212..0000000000 --- a/src/Microsoft.VisualStudio.Editor.Razor/TextSnapshotSourceDocument.cs +++ /dev/null @@ -1,79 +0,0 @@ -// 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.Text; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.VisualStudio.Text; - -namespace Microsoft.VisualStudio.Editor.Razor -{ - internal class TextSnapshotSourceDocument : RazorSourceDocument - { - private readonly ITextSnapshot _buffer; - private readonly RazorSourceLineCollection _lines; - - public TextSnapshotSourceDocument(ITextSnapshot snapshot, string filePath) - { - if (snapshot == null) - { - throw new ArgumentNullException(nameof(snapshot)); - } - - if (filePath == null) - { - throw new ArgumentNullException(nameof(filePath)); - } - - _buffer = snapshot; - FilePath = filePath; - - _lines = new DefaultRazorSourceLineCollection(this); - } - - public override char this[int position] => _buffer[position]; - - public override Encoding Encoding => Encoding.UTF8; - - public override int Length => _buffer.Length; - - public override RazorSourceLineCollection Lines => _lines; - - public override string FilePath { get; } - - public override void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count) - { - if (destination == null) - { - throw new ArgumentNullException(nameof(destination)); - } - - if (sourceIndex < 0) - { - throw new ArgumentOutOfRangeException(nameof(sourceIndex)); - } - - if (destinationIndex < 0) - { - throw new ArgumentOutOfRangeException(nameof(destinationIndex)); - } - - if (count < 0 || count > Length - sourceIndex || count > destination.Length - destinationIndex) - { - throw new ArgumentOutOfRangeException(nameof(count)); - } - - if (count == 0) - { - return; - } - - for (var i = 0; i < count; i++) - { - destination[destinationIndex + i] = this[sourceIndex + i]; - } - } - - public override byte[] GetChecksum() => throw new NotImplementedException(); - } -} \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioRazorParser.cs b/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioRazorParser.cs index e8dd96cf90..c1d71a6111 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioRazorParser.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioRazorParser.cs @@ -11,8 +11,6 @@ namespace Microsoft.VisualStudio.Editor.Razor { public abstract event EventHandler DocumentStructureChanged; - public abstract RazorTemplateEngine TemplateEngine { get; } - public abstract string FilePath { get; } public abstract RazorCodeDocument CodeDocument { get; } diff --git a/src/RazorPageGenerator/Program.cs b/src/RazorPageGenerator/Program.cs index aecb2344dc..f3ae757886 100644 --- a/src/RazorPageGenerator/Program.cs +++ b/src/RazorPageGenerator/Program.cs @@ -31,8 +31,8 @@ Examples: var rootNamespace = args[0]; var targetProjectDirectory = args.Length > 1 ? args[1] : Directory.GetCurrentDirectory(); - var razorEngine = CreateRazorEngine(rootNamespace); - var results = MainCore(razorEngine, targetProjectDirectory); + var projectEngine = CreateProjectEngine(rootNamespace, targetProjectDirectory); + var results = MainCore(projectEngine, targetProjectDirectory); foreach (var result in results) { @@ -45,9 +45,10 @@ Examples: return 0; } - public static RazorEngine CreateRazorEngine(string rootNamespace, Action configure = null) + public static RazorProjectEngine CreateProjectEngine(string rootNamespace, string targetProjectDirectory, Action configure = null) { - var razorEngine = RazorEngine.Create(builder => + var fileSystem = RazorProjectFileSystem.Create(targetProjectDirectory); + var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, fileSystem, builder => { builder .SetNamespace(rootNamespace) @@ -66,20 +67,18 @@ Examples: { configure(builder); } - }); - return razorEngine; - } - public static IList MainCore(RazorEngine razorEngine, string targetProjectDirectory) - { - var viewDirectories = Directory.EnumerateDirectories(targetProjectDirectory, "Views", SearchOption.AllDirectories); - var razorProject = RazorProjectFileSystem.Create(targetProjectDirectory); - var templateEngine = new RazorTemplateEngine(razorEngine, razorProject); - templateEngine.Options.DefaultImports = RazorSourceDocument.Create(@" + builder.AddDefaultImports(@" @using System @using System.Threading.Tasks -", fileName: null); +"); + }); + return projectEngine; + } + public static IList MainCore(RazorProjectEngine projectEngine, string targetProjectDirectory) + { + var viewDirectories = Directory.EnumerateDirectories(targetProjectDirectory, "Views", SearchOption.AllDirectories); var fileCount = 0; var results = new List(); @@ -88,7 +87,7 @@ Examples: Console.WriteLine(); Console.WriteLine(" Generating code files for views in {0}", viewDir); var viewDirPath = viewDir.Substring(targetProjectDirectory.Length).Replace('\\', '/'); - var cshtmlFiles = razorProject.EnumerateItems(viewDirPath); + var cshtmlFiles = projectEngine.FileSystem.EnumerateItems(viewDirPath); if (!cshtmlFiles.Any()) { @@ -99,7 +98,7 @@ Examples: foreach (var item in cshtmlFiles) { Console.WriteLine(" Generating code file for view {0}...", item.FileName); - results.Add(GenerateCodeFile(templateEngine, item)); + results.Add(GenerateCodeFile(projectEngine, item)); Console.WriteLine(" Done!"); fileCount++; } @@ -108,10 +107,11 @@ Examples: return results; } - private static RazorPageGeneratorResult GenerateCodeFile(RazorTemplateEngine templateEngine, RazorProjectItem projectItem) + private static RazorPageGeneratorResult GenerateCodeFile(RazorProjectEngine projectEngine, RazorProjectItem projectItem) { var projectItemWrapper = new FileSystemRazorProjectItemWrapper(projectItem); - var cSharpDocument = templateEngine.GenerateCode(projectItemWrapper); + var codeDocument = projectEngine.Process(projectItemWrapper); + var cSharpDocument = codeDocument.GetCSharpDocument(); if (cSharpDocument.Diagnostics.Any()) { var diagnostics = string.Join(Environment.NewLine, cSharpDocument.Diagnostics); diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/AssemblyAttributeInjectionPassTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/AssemblyAttributeInjectionPassTest.cs index 0c69be0e18..a507103fb7 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/AssemblyAttributeInjectionPassTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/AssemblyAttributeInjectionPassTest.cs @@ -13,7 +13,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions public void Execute_NoOps_IfNamespaceNodeIsMissing() { // Arrange - var irDocument = new DocumentIntermediateNode(); + var irDocument = new DocumentIntermediateNode() + { + Options = RazorCodeGenerationOptions.CreateDefault(), + }; + var pass = new AssemblyAttributeInjectionPass { Engine = RazorEngine.Create(), @@ -30,7 +34,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions public void Execute_NoOps_IfNamespaceNodeHasEmptyContent() { // Arrange - var irDocument = new DocumentIntermediateNode(); + var irDocument = new DocumentIntermediateNode() + { + Options = RazorCodeGenerationOptions.CreateDefault(), + }; var builder = IntermediateNodeBuilder.Create(irDocument); var @namespace = new NamespaceDeclarationIntermediateNode() { Content = string.Empty }; @namespace.Annotations[CommonAnnotations.PrimaryNamespace] = CommonAnnotations.PrimaryNamespace; @@ -53,7 +60,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions public void Execute_NoOps_IfClassNameNodeIsMissing() { // Arrange - var irDocument = new DocumentIntermediateNode(); + var irDocument = new DocumentIntermediateNode() + { + Options = RazorCodeGenerationOptions.CreateDefault(), + }; + var builder = IntermediateNodeBuilder.Create(irDocument); var @namespace = new NamespaceDeclarationIntermediateNode() { Content = "SomeNamespace" }; builder.Push(@namespace); @@ -67,7 +78,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions pass.Execute(TestRazorCodeDocument.CreateEmpty(), irDocument); // Assert - Assert.Collection(irDocument.Children, + Assert.Collection( + irDocument.Children, node => Assert.Same(@namespace, node)); } @@ -75,7 +87,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions public void Execute_NoOps_IfClassNameIsEmpty() { // Arrange - var irDocument = new DocumentIntermediateNode(); + var irDocument = new DocumentIntermediateNode() + { + Options = RazorCodeGenerationOptions.CreateDefault(), + }; var builder = IntermediateNodeBuilder.Create(irDocument); var @namespace = new NamespaceDeclarationIntermediateNode { @@ -115,6 +130,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions var irDocument = new DocumentIntermediateNode { DocumentKind = "Default", + Options = RazorCodeGenerationOptions.CreateDefault(), }; var builder = IntermediateNodeBuilder.Create(irDocument); var @namespace = new NamespaceDeclarationIntermediateNode() { Content = "SomeNamespace" }; @@ -138,7 +154,54 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions pass.Execute(TestRazorCodeDocument.CreateEmpty(), irDocument); // Assert - Assert.Collection(irDocument.Children, + Assert.Collection( + irDocument.Children, + node => Assert.Same(@namespace, node)); + } + + [Fact] + public void Execute_NoOps_ForDesignTime() + { + // Arrange + var irDocument = new DocumentIntermediateNode + { + DocumentKind = MvcViewDocumentClassifierPass.MvcViewDocumentKind, + Options = RazorCodeGenerationOptions.CreateDesignTimeDefault(), + }; + var builder = IntermediateNodeBuilder.Create(irDocument); + var @namespace = new NamespaceDeclarationIntermediateNode + { + Content = "SomeNamespace", + Annotations = + { + [CommonAnnotations.PrimaryNamespace] = CommonAnnotations.PrimaryNamespace + }, + }; + builder.Push(@namespace); + var @class = new ClassDeclarationIntermediateNode + { + ClassName = "SomeName", + Annotations = + { + [CommonAnnotations.PrimaryClass] = CommonAnnotations.PrimaryClass, + }, + }; + builder.Add(@class); + + var pass = new AssemblyAttributeInjectionPass + { + Engine = RazorEngine.Create(), + }; + + var source = TestRazorSourceDocument.Create("test", new RazorSourceDocumentProperties(filePath: null, relativePath: "/Views/Index.cshtml")); + var document = RazorCodeDocument.Create(source); + + // Act + pass.Execute(document, irDocument); + + // Assert + Assert.Collection( + irDocument.Children, node => Assert.Same(@namespace, node)); } @@ -150,6 +213,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions var irDocument = new DocumentIntermediateNode { DocumentKind = MvcViewDocumentClassifierPass.MvcViewDocumentKind, + Options = RazorCodeGenerationOptions.CreateDefault(), }; var builder = IntermediateNodeBuilder.Create(irDocument); var @namespace = new NamespaceDeclarationIntermediateNode @@ -202,6 +266,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions var irDocument = new DocumentIntermediateNode { DocumentKind = MvcViewDocumentClassifierPass.MvcViewDocumentKind, + Options = RazorCodeGenerationOptions.CreateDefault(), }; var builder = IntermediateNodeBuilder.Create(irDocument); var @namespace = new NamespaceDeclarationIntermediateNode @@ -254,6 +319,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions var irDocument = new DocumentIntermediateNode { DocumentKind = RazorPageDocumentClassifierPass.RazorPageDocumentKind, + Options = RazorCodeGenerationOptions.CreateDefault(), }; var builder = IntermediateNodeBuilder.Create(irDocument); var pageDirective = new DirectiveIntermediateNode @@ -313,6 +379,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions var irDocument = new DocumentIntermediateNode { DocumentKind = MvcViewDocumentClassifierPass.MvcViewDocumentKind, + Options = RazorCodeGenerationOptions.CreateDefault(), }; var builder = IntermediateNodeBuilder.Create(irDocument); var @namespace = new NamespaceDeclarationIntermediateNode diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/InstrumentationPassTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/InstrumentationPassTest.cs index 0827042ac4..d59d1ad06b 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/InstrumentationPassTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/InstrumentationPassTest.cs @@ -10,11 +10,47 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions { public class InstrumentationPassTest { + [Fact] + public void InstrumentationPass_NoOps_ForDesignTime() + { + // Arrange + var document = new DocumentIntermediateNode() + { + Options = RazorCodeGenerationOptions.CreateDesignTimeDefault(), + }; + + var builder = IntermediateNodeBuilder.Create(document); + builder.Push(new HtmlContentIntermediateNode()); + builder.Add(new IntermediateToken() + { + Content = "Hi", + Kind = TokenKind.Html, + }); + builder.Pop(); + + var pass = new InstrumentationPass() + { + Engine = RazorEngine.CreateEmpty(b => { }), + }; + + // Act + pass.Execute(TestRazorCodeDocument.CreateEmpty(), document); + + // Assert + Children( + document, + n => IntermediateNodeAssert.Html("Hi", n)); + } + [Fact] public void InstrumentationPass_InstrumentsHtml() { // Arrange - var document = new DocumentIntermediateNode(); + var document = new DocumentIntermediateNode() + { + Options = RazorCodeGenerationOptions.CreateDefault(), + }; + var builder = IntermediateNodeBuilder.Create(document); builder.Push(new HtmlContentIntermediateNode() @@ -49,7 +85,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions public void InstrumentationPass_SkipsHtml_WithoutLocation() { // Arrange - var document = new DocumentIntermediateNode(); + var document = new DocumentIntermediateNode() + { + Options = RazorCodeGenerationOptions.CreateDefault(), + }; + var builder = IntermediateNodeBuilder.Create(document); builder.Push(new HtmlContentIntermediateNode()); builder.Add(new IntermediateToken() @@ -77,7 +117,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions public void InstrumentationPass_InstrumentsCSharpExpression() { // Arrange - var document = new DocumentIntermediateNode(); + var document = new DocumentIntermediateNode() + { + Options = RazorCodeGenerationOptions.CreateDefault(), + }; + var builder = IntermediateNodeBuilder.Create(document); builder.Push(new CSharpExpressionIntermediateNode() { @@ -109,7 +153,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions public void InstrumentationPass_SkipsCSharpExpression_WithoutLocation() { // Arrange - var document = new DocumentIntermediateNode(); + var document = new DocumentIntermediateNode() + { + Options = RazorCodeGenerationOptions.CreateDefault(), + }; + var builder = IntermediateNodeBuilder.Create(document); builder.Push(new CSharpExpressionIntermediateNode()); builder.Add(new IntermediateToken() @@ -136,7 +184,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions public void InstrumentationPass_SkipsCSharpExpression_InsideTagHelperAttribute() { // Arrange - var document = new DocumentIntermediateNode(); + var document = new DocumentIntermediateNode() + { + Options = RazorCodeGenerationOptions.CreateDefault(), + }; + var builder = IntermediateNodeBuilder.Create(document); builder.Push(new TagHelperIntermediateNode()); @@ -183,7 +235,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions public void InstrumentationPass_SkipsCSharpExpression_InsideTagHelperProperty() { // Arrange - var document = new DocumentIntermediateNode(); + var document = new DocumentIntermediateNode() + { + Options = RazorCodeGenerationOptions.CreateDefault(), + }; + var builder = IntermediateNodeBuilder.Create(document); builder.Push(new TagHelperIntermediateNode()); @@ -230,7 +286,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions public void InstrumentationPass_InstrumentsTagHelper() { // Arrange - var document = new DocumentIntermediateNode(); + var document = new DocumentIntermediateNode() + { + Options = RazorCodeGenerationOptions.CreateDefault(), + }; + var builder = IntermediateNodeBuilder.Create(document); builder.Add(new TagHelperIntermediateNode() { @@ -257,7 +317,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions public void InstrumentationPass_SkipsTagHelper_WithoutLocation() { // Arrange - var document = new DocumentIntermediateNode(); + var document = new DocumentIntermediateNode() + { + Options = RazorCodeGenerationOptions.CreateDefault(), + }; + var builder = IntermediateNodeBuilder.Create(document); builder.Push(new TagHelperIntermediateNode()); diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/ModelDirectiveTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/ModelDirectiveTest.cs index fcf8bc2422..f6466520af 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/ModelDirectiveTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/ModelDirectiveTest.cs @@ -61,7 +61,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions "); var engine = CreateEngine(); - var pass = new ModelDirective.Pass(designTime: false) + var pass = new ModelDirective.Pass() { Engine = engine, }; @@ -88,7 +88,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions "); var engine = CreateEngine(); - var pass = new ModelDirective.Pass(designTime: false) + var pass = new ModelDirective.Pass() { Engine = engine, }; @@ -114,7 +114,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions "); var engine = CreateEngine(); - var pass = new ModelDirective.Pass(designTime: false) + var pass = new ModelDirective.Pass() { Engine = engine, }; @@ -139,7 +139,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions "); var engine = CreateEngine(); - var pass = new ModelDirective.Pass(designTime: false) + var pass = new ModelDirective.Pass() { Engine = engine, }; @@ -163,8 +163,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions @inherits BaseType "); - var engine = CreateEngine(); - var pass = new ModelDirective.Pass(designTime: true) + var engine = CreateDesignTimeEngine(); + var pass = new ModelDirective.Pass() { Engine = engine, }; @@ -193,8 +193,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions @model SomeType "); - var engine = CreateEngine(); - var pass = new ModelDirective.Pass(designTime: true) + var engine = CreateDesignTimeEngine(); + var pass = new ModelDirective.Pass() { Engine = engine, }; @@ -246,6 +246,18 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions }); } + private RazorEngine CreateDesignTimeEngine() + { + return RazorEngine.CreateDesignTime(b => + { + // Notice we're not registering the ModelDirective.Pass here so we can run it on demand. + b.AddDirective(ModelDirective.Directive); + + // There's some special interaction with the inherits directive + InheritsDirective.Register(b); + }); + } + private DocumentIntermediateNode CreateIRDocument(RazorEngine engine, RazorCodeDocument codeDocument) { for (var i = 0; i < engine.Phases.Count; i++) diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/DefaultMvcImportFeatureTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/MvcImportProjectFeatureTest.cs similarity index 85% rename from test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/DefaultMvcImportFeatureTest.cs rename to test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/MvcImportProjectFeatureTest.cs index 3ae6d8dd13..7e811ee261 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/DefaultMvcImportFeatureTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/MvcImportProjectFeatureTest.cs @@ -8,16 +8,16 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.Razor.Extensions { - public class DefaultMvcImportFeatureTest + public class MvcImportProjectFeatureTest { [Fact] public void AddDefaultDirectivesImport_AddsSingleDynamicImport() { // Arrange - var imports = new List(); + var imports = new List(); // Act - DefaultMvcImportFeature.AddDefaultDirectivesImport(imports); + MvcImportProjectFeature.AddDefaultDirectivesImport(imports); // Assert var import = Assert.Single(imports); @@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions public void AddHierarchicalImports_AddsViewImportSourceDocumentsOnDisk() { // Arrange - var imports = new List(); + var imports = new List(); var projectItem = new TestRazorProjectItem("/Contact/Index.cshtml"); var testFileSystem = new TestRazorProjectFileSystem(new[] { @@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions new TestRazorProjectItem("/Contact/_ViewImports.cshtml"), projectItem, }); - var mvcImportFeature = new DefaultMvcImportFeature() + var mvcImportFeature = new MvcImportProjectFeature() { ProjectEngine = Mock.Of(projectEngine => projectEngine.FileSystem == testFileSystem) }; @@ -55,10 +55,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions public void AddHierarchicalImports_AddsViewImportSourceDocumentsNotOnDisk() { // Arrange - var imports = new List(); + var imports = new List(); var projectItem = new TestRazorProjectItem("/Pages/Contact/Index.cshtml"); var testFileSystem = new TestRazorProjectFileSystem(new[] { projectItem }); - var mvcImportFeature = new DefaultMvcImportFeature() + var mvcImportFeature = new MvcImportProjectFeature() { ProjectEngine = Mock.Of(projectEngine => projectEngine.FileSystem == testFileSystem) }; diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X.Test/ModelDirectiveTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X.Test/ModelDirectiveTest.cs index c366918999..c0319a9a0a 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X.Test/ModelDirectiveTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X.Test/ModelDirectiveTest.cs @@ -61,7 +61,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X "); var engine = CreateEngine(); - var pass = new ModelDirective.Pass(designTime: false) + var pass = new ModelDirective.Pass() { Engine = engine, }; @@ -88,7 +88,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X "); var engine = CreateEngine(); - var pass = new ModelDirective.Pass(designTime: false) + var pass = new ModelDirective.Pass() { Engine = engine, }; @@ -114,7 +114,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X "); var engine = CreateEngine(); - var pass = new ModelDirective.Pass(designTime: false) + var pass = new ModelDirective.Pass() { Engine = engine, }; @@ -139,7 +139,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X "); var engine = CreateEngine(); - var pass = new ModelDirective.Pass(designTime: false) + var pass = new ModelDirective.Pass() { Engine = engine, }; @@ -163,8 +163,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X @inherits BaseType "); - var engine = CreateEngine(); - var pass = new ModelDirective.Pass(designTime: true) + var engine = CreateDesignTimeEngine(); + var pass = new ModelDirective.Pass() { Engine = engine, }; @@ -193,8 +193,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X @model SomeType "); - var engine = CreateEngine(); - var pass = new ModelDirective.Pass(designTime: true) + var engine = CreateDesignTimeEngine(); + var pass = new ModelDirective.Pass() { Engine = engine, }; @@ -246,6 +246,18 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X }); } + private RazorEngine CreateDesignTimeEngine() + { + return RazorEngine.CreateDesignTime(b => + { + // Notice we're not registering the ModelDirective.Pass here so we can run it on demand. + b.AddDirective(ModelDirective.Directive); + + // There's some special interaction with the inherits directive + InheritsDirective.Register(b); + }); + } + private DocumentIntermediateNode CreateIRDocument(RazorEngine engine, RazorCodeDocument codeDocument) { for (var i = 0; i < engine.Phases.Count; i++) diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X.Test/DefaultMvcImportFeatureTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X.Test/MvcImportProjectFeatureTest.cs similarity index 86% rename from test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X.Test/DefaultMvcImportFeatureTest.cs rename to test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X.Test/MvcImportProjectFeatureTest.cs index 3741760b4a..e53046ed5a 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X.Test/DefaultMvcImportFeatureTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X.Test/MvcImportProjectFeatureTest.cs @@ -8,16 +8,16 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X { - public class DefaultMvcImportFeatureTest + public class MvcImportProjectFeatureTest { [Fact] public void AddDefaultDirectivesImport_AddsSingleDynamicImport() { // Arrange - var imports = new List(); + var imports = new List(); // Act - DefaultMvcImportFeature.AddDefaultDirectivesImport(imports); + MvcImportProjectFeature.AddDefaultDirectivesImport(imports); // Assert var import = Assert.Single(imports); @@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X public void AddHierarchicalImports_AddsViewImportSourceDocumentsOnDisk() { // Arrange - var imports = new List(); + var imports = new List(); var projectItem = new TestRazorProjectItem("/Contact/Index.cshtml"); var testFileSystem = new TestRazorProjectFileSystem(new[] { @@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X new TestRazorProjectItem("/Contact/_ViewImports.cshtml"), projectItem, }); - var mvcImportFeature = new DefaultMvcImportFeature() + var mvcImportFeature = new MvcImportProjectFeature() { ProjectEngine = Mock.Of(projectEngine => projectEngine.FileSystem == testFileSystem) }; @@ -55,10 +55,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X public void AddHierarchicalImports_AddsViewImportSourceDocumentsNotOnDisk() { // Arrange - var imports = new List(); + var imports = new List(); var projectItem = new TestRazorProjectItem("/Pages/Contact/Index.cshtml"); var testFileSystem = new TestRazorProjectFileSystem(new[] { projectItem }); - var mvcImportFeature = new DefaultMvcImportFeature() + var mvcImportFeature = new MvcImportProjectFeature() { ProjectEngine = Mock.Of(projectEngine => projectEngine.FileSystem == testFileSystem) }; diff --git a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/Assert.cs b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/Assert.cs index 37427a1cc3..55791a7284 100644 --- a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/Assert.cs +++ b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/Assert.cs @@ -100,6 +100,59 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests throw new BuildOutputMissingException(result, match); } + public static void FileContains(MSBuildResult result, string filePath, string match) + { + if (result == null) + { + throw new ArgumentNullException(nameof(result)); + } + + filePath = Path.Combine(result.Project.DirectoryPath, filePath); + FileExists(result, filePath); + + var text = File.ReadAllText(filePath); + if (text.Contains(match)) + { + return; + } + + throw new FileContentMissingException(result, filePath, File.ReadAllText(filePath), match); + } + + public static void FileDoesNotContain(MSBuildResult result, string filePath, string match) + { + if (result == null) + { + throw new ArgumentNullException(nameof(result)); + } + + filePath = Path.Combine(result.Project.DirectoryPath, filePath); + FileExists(result, filePath); + + var text = File.ReadAllText(filePath); + if (text.Contains(match)) + { + throw new FileContentFoundException(result, filePath, File.ReadAllText(filePath), match); + } + } + + public static void FileContentEquals(MSBuildResult result, string filePath, string expected) + { + if (result == null) + { + throw new ArgumentNullException(nameof(result)); + } + + filePath = Path.Combine(result.Project.DirectoryPath, filePath); + FileExists(result, filePath); + + var actual = File.ReadAllText(filePath); + if (!actual.Equals(expected, StringComparison.Ordinal)) + { + throw new FileContentNotEqualException(result, filePath, expected, actual); + } + } + public static void FileContainsLine(MSBuildResult result, string filePath, string match) { if (result == null) @@ -233,7 +286,36 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests } } - public static void NupkgContains(MSBuildResult result, string nupkgPath, string filePath) + public static void NuspecDoesNotContain(MSBuildResult result, string nuspecPath, string expected) + { + if (result == null) + { + throw new ArgumentNullException(nameof(result)); + } + + if (nuspecPath == null) + { + throw new ArgumentNullException(nameof(nuspecPath)); + } + + if (expected == null) + { + throw new ArgumentNullException(nameof(expected)); + } + + nuspecPath = Path.Combine(result.Project.DirectoryPath, nuspecPath); + FileExists(result, nuspecPath); + + var content = File.ReadAllText(nuspecPath); + if (content.Contains(expected)) + { + throw new NuspecFoundException(result, nuspecPath, content, expected); + } + } + + // This method extracts the nupkg to a fixed directory path. To avoid the extra work of + // cleaning up after each invocation, this method accepts multiple files. + public static void NupkgContains(MSBuildResult result, string nupkgPath, params string[] filePaths) { if (result == null) { @@ -245,20 +327,23 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests throw new ArgumentNullException(nameof(nupkgPath)); } - if (filePath == null) + if (filePaths == null) { - throw new ArgumentNullException(nameof(filePath)); + throw new ArgumentNullException(nameof(filePaths)); } nupkgPath = Path.Combine(result.Project.DirectoryPath, nupkgPath); FileExists(result, nupkgPath); - var unzipped = Path.Combine(result.Project.DirectoryPath, "nupkg"); + var unzipped = Path.Combine(result.Project.DirectoryPath, Path.GetFileNameWithoutExtension(nupkgPath)); ZipFile.ExtractToDirectory(nupkgPath, unzipped); - if (!File.Exists(Path.Combine(unzipped, filePath))) + foreach (var filePath in filePaths) { - throw new NupkgFileMissingException(result, nupkgPath, filePath); + if (!File.Exists(Path.Combine(unzipped, filePath))) + { + throw new NupkgFileMissingException(result, nupkgPath, filePath); + } } } @@ -407,6 +492,36 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests } } + private class FileContentNotEqualException : MSBuildXunitException + { + public FileContentNotEqualException(MSBuildResult result, string filePath, string expected, string actual) + : base(result) + { + FilePath = filePath; + Expected = expected; + Actual = actual; + } + + public string Actual { get; } + + public string FilePath { get; } + + public string Expected { get; } + + protected override string Heading + { + get + { + var builder = new StringBuilder(); + builder.AppendFormat("File content of '{0}' did not match the expected content: '{1}'.", FilePath, Expected); + builder.AppendLine(); + builder.AppendLine(); + builder.AppendLine(Actual); + return builder.ToString(); + } + } + } + private class FileMissingException : MSBuildXunitException { public FileMissingException(MSBuildResult result, string filePath) @@ -507,6 +622,34 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests } } + private class NuspecFoundException : MSBuildXunitException + { + public NuspecFoundException(MSBuildResult result, string filePath, string content, string expected) + : base(result) + { + FilePath = filePath; + Content = content; + Expected = expected; + } + + public string Content { get; } + + public string Expected { get; } + + public string FilePath { get; } + + protected override string Heading + { + get + { + return + $"nuspec: '{FilePath}' should not contain the content {Expected}." + + Environment.NewLine + + $"actual content: {Content}"; + } + } + } + private class NupkgFileMissingException : MSBuildXunitException { public NupkgFileMissingException(MSBuildResult result, string nupkgPath, string filePath) diff --git a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildIncrementalismTest.cs b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildIncrementalismTest.cs new file mode 100644 index 0000000000..53192cf8cf --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildIncrementalismTest.cs @@ -0,0 +1,94 @@ +// 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.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests +{ + public class BuildIncrementalismTest : MSBuildIntegrationTestBase, IClassFixture + { + public BuildIncrementalismTest(BuildServerTestFixture buildServer) + : base(buildServer) + { + } + + [Fact] + [InitializeTestProject("SimpleMvc")] + public async Task BuildIncremental_SimpleMvc_PersistsTargetInputFile() + { + // Arrange + var thumbprintLookup = new Dictionary(); + + // Act 1 + var result = await DotnetMSBuild("Build"); + + var directoryPath = Path.Combine(result.Project.DirectoryPath, IntermediateOutputPath); + var filesToIgnore = new[] + { + // These files are generated on every build. + Path.Combine(directoryPath, "SimpleMvc.csproj.CopyComplete"), + Path.Combine(directoryPath, "SimpleMvc.csproj.FileListAbsolute.txt"), + }; + var files = Directory.GetFiles(directoryPath).Where(p => !filesToIgnore.Contains(p)); + foreach (var file in files) + { + var thumbprint = GetThumbPrint(file); + thumbprintLookup[file] = thumbprint; + } + + // Assert 1 + Assert.BuildPassed(result); + + // Act & Assert 2 + for (var i = 0; i < 2; i++) + { + // We want to make sure nothing changed between multiple incremental builds. + using (var razorGenDirectoryLock = LockDirectory(RazorIntermediateOutputPath)) + { + result = await DotnetMSBuild("Build"); + } + + Assert.BuildPassed(result); + foreach (var file in files) + { + var thumbprint = GetThumbPrint(file); + Assert.Equal(thumbprintLookup[file], thumbprint); + } + } + } + + [Fact] + [InitializeTestProject("SimpleMvc")] + public async Task RazorGenerate_RegeneratesTagHelperInputs_IfFileChanges() + { + // Act - 1 + var expectedTagHelperCacheContent = @"""Name"":""SimpleMvc.SimpleTagHelper"""; + var result = await DotnetMSBuild("Build"); + var file = Path.Combine(Project.DirectoryPath, "SimpleTagHelper.cs"); + var tagHelperOutputCache = Path.Combine(IntermediateOutputPath, "SimpleMvc.TagHelpers.output.cache"); + var generatedFile = Path.Combine(RazorIntermediateOutputPath, "Views", "Home", "Index.cs"); + + // Assert - 1 + Assert.BuildPassed(result); + Assert.FileContains(result, tagHelperOutputCache, expectedTagHelperCacheContent); + var fileThumbPrint = GetThumbPrint(generatedFile); + + // Act - 2 + // Update the source content and build. We should expect the outputs to be regenerated. + ReplaceContent(string.Empty, file); + result = await DotnetMSBuild("Build"); + + // Assert - 2 + Assert.BuildPassed(result); + Assert.FileContentEquals(result, tagHelperOutputCache, "[]"); + var newThumbPrint = GetThumbPrint(generatedFile); + Assert.NotEqual(fileThumbPrint, newThumbPrint); + } + + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildIntegrationTest.cs b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildIntegrationTest.cs index 30cbfd1b86..b5a6f75bc8 100644 --- a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildIntegrationTest.cs @@ -2,42 +2,54 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.IO; +using System.Linq; +using System.Reflection; using System.Runtime.InteropServices; using System.Threading.Tasks; using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Extensions.DependencyModel; using Xunit; namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests { - public class BuildIntegrationTest : MSBuildIntegrationTestBase + public class BuildIntegrationTest : MSBuildIntegrationTestBase, IClassFixture { + public BuildIntegrationTest(BuildServerTestFixture buildServer) + : base(buildServer) + { + } + [Fact] [InitializeTestProject("SimpleMvc")] - public Task Build_SimpleMvc_UsingDotnetMSBuild_CanBuildSuccessfully() - => Build_SimpleMvc_CanBuildSuccessfully(MSBuildProcessKind.Dotnet); + public Task Build_SimpleMvc_UsingDotnetMSBuildAndWithoutBuildServer_CanBuildSuccessfully() + => Build_SimpleMvc_WithoutBuildServer_CanBuildSuccessfully(MSBuildProcessKind.Dotnet); [ConditionalFact] [OSSkipCondition(OperatingSystems.Linux)] [OSSkipCondition(OperatingSystems.MacOSX)] [InitializeTestProject("SimpleMvc")] - public Task Build_SimpleMvc_UsingDesktopMSBuild_CanBuildSuccessfully() - => Build_SimpleMvc_CanBuildSuccessfully(MSBuildProcessKind.Desktop); + public Task Build_SimpleMvc_UsingDesktopMSBuildAndWithoutBuildServer_CanBuildSuccessfully() + => Build_SimpleMvc_WithoutBuildServer_CanBuildSuccessfully(MSBuildProcessKind.Desktop); - private async Task Build_SimpleMvc_CanBuildSuccessfully(MSBuildProcessKind msBuildProcessKind) + // This test is identical to the ones in BuildServerIntegrationTest except this one explicitly disables the Razor build server. + private async Task Build_SimpleMvc_WithoutBuildServer_CanBuildSuccessfully(MSBuildProcessKind msBuildProcessKind) { - var result = await DotnetMSBuild("Build", "/p:RazorCompileOnBuild=true", msBuildProcessKind: msBuildProcessKind); + var result = await DotnetMSBuild("Build", + "/p:UseRazorBuildServer=false", + suppressBuildServer: true, + msBuildProcessKind: msBuildProcessKind); Assert.BuildPassed(result); Assert.FileExists(result, OutputPath, "SimpleMvc.dll"); Assert.FileExists(result, OutputPath, "SimpleMvc.pdb"); - Assert.FileExists(result, OutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileExists(result, OutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileExists(result, OutputPath, "SimpleMvc.Views.dll"); + Assert.FileExists(result, OutputPath, "SimpleMvc.Views.pdb"); if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { // GetFullPath on OSX doesn't work well in travis. We end up computing a different path than will // end up in the MSBuild logs. - Assert.BuildOutputContainsLine(result, $"SimpleMvc -> {Path.Combine(Path.GetFullPath(Project.DirectoryPath), OutputPath, "SimpleMvc.PrecompiledViews.dll")}"); + Assert.BuildOutputContainsLine(result, $"SimpleMvc -> {Path.Combine(Path.GetFullPath(Project.DirectoryPath), OutputPath, "SimpleMvc.Views.dll")}"); } } @@ -47,26 +59,26 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests { Directory.Delete(Path.Combine(Project.DirectoryPath, "Views"), recursive: true); - var result = await DotnetMSBuild("Build", "/p:RazorCompileOnBuild=true"); + var result = await DotnetMSBuild("Build"); Assert.BuildPassed(result); Assert.FileExists(result, OutputPath, "SimpleMvc.dll"); Assert.FileExists(result, OutputPath, "SimpleMvc.pdb"); - Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.Views.dll"); + Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.Views.pdb"); } [Fact] [InitializeTestProject("SimpleMvc")] - public async Task Build_SimpleMvc_NoopsWithRazorCompileOnPublish() + public async Task Build_SimpleMvc_NoopsWithRazorCompileOnBuild_False() { - var result = await DotnetMSBuild("Build", "/p:RazorCompileOnPublish=true"); + var result = await DotnetMSBuild("Build", "/p:RazorCompileOnBuild=false"); Assert.BuildPassed(result); Assert.FileExists(result, OutputPath, "SimpleMvc.dll"); Assert.FileExists(result, OutputPath, "SimpleMvc.pdb"); - Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.Views.dll"); + Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.Views.pdb"); } [Fact] @@ -76,7 +88,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests // Introducing a C# semantic error ReplaceContent("@{ var foo = \"\".Substring(\"bleh\"); }", "Views", "Home", "Index.cshtml"); - var result = await DotnetMSBuild("Build", "/p:RazorCompileOnBuild=true"); + var result = await DotnetMSBuild("Build"); Assert.BuildFailed(result); @@ -85,19 +97,19 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests // Compilation failed without creating the views assembly Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.dll"); - Assert.FileDoesNotExist(result, IntermediateOutputPath, "SimpleMvc.PrecompiledViews.dll"); + Assert.FileDoesNotExist(result, IntermediateOutputPath, "SimpleMvc.Views.dll"); } [Fact] [InitializeTestProject("SimplePages")] public async Task Build_Works_WhenFilesAtDifferentPathsHaveSameNamespaceHierarchy() { - var result = await DotnetMSBuild("Build", "/p:RazorCompileOnBuild=true"); + var result = await DotnetMSBuild("Build"); Assert.BuildPassed(result); Assert.FileExists(result, IntermediateOutputPath, "SimplePages.dll"); - Assert.FileExists(result, IntermediateOutputPath, "SimplePages.PrecompiledViews.dll"); + Assert.FileExists(result, IntermediateOutputPath, "SimplePages.Views.dll"); } [Fact] @@ -105,15 +117,15 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests public async Task Build_RazorOutputPath_SetToNonDefault() { var customOutputPath = Path.Combine("bin", Configuration, TargetFramework, "Razor"); - var result = await DotnetMSBuild("Build", $"/p:RazorCompileOnBuild=true /p:RazorOutputPath={customOutputPath}"); + var result = await DotnetMSBuild("Build", $"/p:RazorOutputPath={customOutputPath}"); Assert.BuildPassed(result); - Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.Views.dll"); + Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.Views.pdb"); - Assert.FileExists(result, customOutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileExists(result, customOutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileExists(result, customOutputPath, "SimpleMvc.Views.dll"); + Assert.FileExists(result, customOutputPath, "SimpleMvc.Views.pdb"); } [Fact] @@ -121,78 +133,78 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests public async Task Build_MvcRazorOutputPath_SetToNonDefault() { var customOutputPath = Path.Combine("bin", Configuration, TargetFramework, "Razor"); - var result = await DotnetMSBuild("Build", $"/p:RazorCompileOnBuild=true /p:MvcRazorOutputPath={customOutputPath}"); + var result = await DotnetMSBuild("Build", $"/p:MvcRazorOutputPath={customOutputPath}"); Assert.BuildPassed(result); - Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.Views.dll"); + Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.Views.pdb"); - Assert.FileExists(result, customOutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileExists(result, customOutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileExists(result, customOutputPath, "SimpleMvc.Views.dll"); + Assert.FileExists(result, customOutputPath, "SimpleMvc.Views.pdb"); } [Fact] [InitializeTestProject("SimpleMvc")] public async Task Build_SkipsCopyingBinariesToOutputDirectory_IfCopyBuildOutputToOutputDirectory_IsUnset() { - var result = await DotnetMSBuild("Build", "/p:RazorCompileOnBuild=true /p:CopyBuildOutputToOutputDirectory=false"); + var result = await DotnetMSBuild("Build", "/p:CopyBuildOutputToOutputDirectory=false"); Assert.BuildPassed(result); Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.dll"); - Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.PrecompiledViews.dll"); + Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.Views.dll"); Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.dll"); - Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.PrecompiledViews.dll"); + Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.Views.dll"); } [Fact] [InitializeTestProject("SimpleMvc")] public async Task Build_SkipsCopyingBinariesToOutputDirectory_IfCopyOutputSymbolsToOutputDirectory_IsUnset() { - var result = await DotnetMSBuild("Build", "/p:RazorCompileOnBuild=true /p:CopyOutputSymbolsToOutputDirectory=false"); + var result = await DotnetMSBuild("Build", "/p:CopyOutputSymbolsToOutputDirectory=false"); Assert.BuildPassed(result); - Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.Views.pdb"); - Assert.FileExists(result, OutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileExists(result, OutputPath, "SimpleMvc.Views.dll"); + Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.Views.pdb"); } [Fact] [InitializeTestProject("SimpleMvc")] public async Task Build_Works_WhenSymbolsAreNotGenerated() { - var result = await DotnetMSBuild("Build", "/p:RazorCompileOnBuild=true /p:DebugType=none"); + var result = await DotnetMSBuild("Build", "/p:DebugType=none"); Assert.BuildPassed(result); Assert.FileDoesNotExist(result, IntermediateOutputPath, "SimpleMvc.pdb"); - Assert.FileDoesNotExist(result, IntermediateOutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileDoesNotExist(result, IntermediateOutputPath, "SimpleMvc.Views.pdb"); - Assert.FileExists(result, OutputPath, "SimpleMvc.PrecompiledViews.dll"); + Assert.FileExists(result, OutputPath, "SimpleMvc.Views.dll"); Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.pdb"); - Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.Views.pdb"); } [Fact] [InitializeTestProject("AppWithP2PReference", "ClassLibrary")] public async Task Build_WithP2P_CopiesRazorAssembly() { - var result = await DotnetMSBuild("Build", "/p:RazorCompileOnBuild=true"); + var result = await DotnetMSBuild("Build"); Assert.BuildPassed(result); Assert.FileExists(result, OutputPath, "AppWithP2PReference.dll"); Assert.FileExists(result, OutputPath, "AppWithP2PReference.pdb"); - Assert.FileExists(result, OutputPath, "AppWithP2PReference.PrecompiledViews.dll"); - Assert.FileExists(result, OutputPath, "AppWithP2PReference.PrecompiledViews.pdb"); + Assert.FileExists(result, OutputPath, "AppWithP2PReference.Views.dll"); + Assert.FileExists(result, OutputPath, "AppWithP2PReference.Views.pdb"); Assert.FileExists(result, OutputPath, "ClassLibrary.dll"); Assert.FileExists(result, OutputPath, "ClassLibrary.pdb"); - Assert.FileExists(result, OutputPath, "ClassLibrary.PrecompiledViews.dll"); - Assert.FileExists(result, OutputPath, "ClassLibrary.PrecompiledViews.pdb"); + Assert.FileExists(result, OutputPath, "ClassLibrary.Views.dll"); + Assert.FileExists(result, OutputPath, "ClassLibrary.Views.pdb"); } [Fact] @@ -208,7 +220,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests AddProjectFileContent(additionalProjectContent); Directory.CreateDirectory(Path.Combine(Project.DirectoryPath, "..", "LinkedDir")); - var result = await DotnetMSBuild("Build", "/t:_IntrospectRazorEmbeddedResources /p:RazorCompileOnBuild=true /p:EmbedRazorGenerateSources=true"); + var result = await DotnetMSBuild("Build", "/t:_IntrospectRazorEmbeddedResources /p:EmbedRazorGenerateSources=true"); Assert.BuildPassed(result); @@ -216,5 +228,64 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests Assert.BuildOutputContainsLine(result, $@"CompileResource: {Path.Combine("Areas", "Products", "Pages", "_ViewStart.cshtml")} /Areas/Products/Pages/_ViewStart.cshtml"); Assert.BuildOutputContainsLine(result, $@"CompileResource: {Path.Combine("..", "LinkedDir", "LinkedFile.cshtml")} /LinkedFileOut/LinkedFile.cshtml"); } + + [Fact] + [InitializeTestProject("SimpleMvc")] + public async Task Build_WithViews_ProducesDepsFileWithCompilationContext() + { + var customDefine = "RazorSdkTest"; + var result = await DotnetMSBuild("Build", $"/p:DefineConstants={customDefine}"); + + Assert.BuildPassed(result); + + Assert.FileExists(result, OutputPath, "SimpleMvc.deps.json"); + var depsFilePath = Path.Combine(Project.DirectoryPath, OutputPath, "SimpleMvc.deps.json"); + var dependencyContext = ReadDependencyContext(depsFilePath); + // Pick a couple of libraries and ensure they have some compile references + var packageReference = dependencyContext.CompileLibraries.First(l => l.Name == "Microsoft.AspNetCore.Html.Abstractions"); + Assert.NotEmpty(packageReference.Assemblies); + + var projectReference = dependencyContext.CompileLibraries.First(l => l.Name == "SimpleMvc"); + Assert.NotEmpty(packageReference.Assemblies); + + Assert.Contains(customDefine, dependencyContext.CompilationOptions.Defines); + } + + [Fact] + [InitializeTestProject("SimpleMvc")] + public async Task Build_WithoutViews_ProducesDepsFileWithotCompiilationContext() + { + Directory.Delete(Path.Combine(Project.DirectoryPath, "Views"), recursive: true); + var customDefine = "RazorSdkTest"; + var result = await DotnetMSBuild("Build", $"/p:DefineConstants={customDefine}"); + + Assert.BuildPassed(result); + + Assert.FileExists(result, OutputPath, "SimpleMvc.deps.json"); + var depsFilePath = Path.Combine(Project.DirectoryPath, OutputPath, "SimpleMvc.deps.json"); + var dependencyContext = ReadDependencyContext(depsFilePath); + Assert.All(dependencyContext.CompileLibraries, library => Assert.Empty(library.Assemblies)); + Assert.Empty(dependencyContext.CompilationOptions.Defines); + } + + [Fact] + [InitializeTestProject("ClassLibrary")] + public async Task Build_ClassLibrary_DoesNotProduceDepsFile() + { + var result = await DotnetMSBuild("Build"); + + Assert.BuildPassed(result); + + Assert.FileDoesNotExist(result, OutputPath, "ClassLibrary.deps.json"); + } + + private static DependencyContext ReadDependencyContext(string depsFilePath) + { + var reader = new DependencyContextJsonReader(); + using (var stream = File.OpenRead(depsFilePath)) + { + return reader.Read(stream); + } + } } } diff --git a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildIntrospectionTest.cs b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildIntrospectionTest.cs new file mode 100644 index 0000000000..0e6875ee22 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildIntrospectionTest.cs @@ -0,0 +1,28 @@ +// 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 System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests +{ + public class BuildIntrospectionTest : MSBuildIntegrationTestBase, IClassFixture + { + public BuildIntrospectionTest(BuildServerTestFixture buildServer) + : base(buildServer) + { + } + + [Fact] + [InitializeTestProject("SimpleMvc")] + public async Task RazorSdk_AddsCshtmlFilesToUpToDateCheckInput() + { + var result = await DotnetMSBuild("_IntrospectUpToDateCheckInput"); + + Assert.BuildPassed(result); + Assert.BuildOutputContainsLine(result, $"UpToDateCheckInput: {Path.Combine("Views", "Home", "Index.cshtml")}"); + Assert.BuildOutputContainsLine(result, $"UpToDateCheckInput: {Path.Combine("Views", "_ViewStart.cshtml")}"); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildServerIntegrationTest.cs b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildServerIntegrationTest.cs index d7fc51b4dd..7ffbbc27e9 100644 --- a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildServerIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildServerIntegrationTest.cs @@ -1,18 +1,18 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.IO; using System.Threading.Tasks; +using Microsoft.AspNetCore.Testing.xunit; using Xunit; namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests { public class BuildServerIntegrationTest : MSBuildIntegrationTestBase, IClassFixture { - private readonly string _pipeName; - - public BuildServerIntegrationTest(BuildServerTestFixture fixture) + public BuildServerIntegrationTest(BuildServerTestFixture buildServer) + : base(buildServer) { - _pipeName = fixture.PipeName; } [Fact] @@ -20,18 +20,35 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests public Task Build_SimpleMvc_WithServer_UsingDotnetMSBuild_CanBuildSuccessfully() => Build_SimpleMvc_CanBuildSuccessfully(MSBuildProcessKind.Dotnet); + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)] + [InitializeTestProject("SimpleMvc")] + public Task Build_SimpleMvc_WithServer_UsingDesktopMSBuild_CanBuildSuccessfully() + => Build_SimpleMvc_CanBuildSuccessfully(MSBuildProcessKind.Desktop); + private async Task Build_SimpleMvc_CanBuildSuccessfully(MSBuildProcessKind msBuildProcessKind) { var result = await DotnetMSBuild( "Build", - $"/p:RazorCompileOnBuild=true /p:UseRazorBuildServer=true /p:_RazorBuildServerPipeName={_pipeName} /p:_RazorForceBuildServer=true", + "/p:_RazorForceBuildServer=true", msBuildProcessKind: msBuildProcessKind); Assert.BuildPassed(result); Assert.FileExists(result, OutputPath, "SimpleMvc.dll"); Assert.FileExists(result, OutputPath, "SimpleMvc.pdb"); - Assert.FileExists(result, OutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileExists(result, OutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileExists(result, OutputPath, "SimpleMvc.Views.dll"); + Assert.FileExists(result, OutputPath, "SimpleMvc.Views.pdb"); + + // Verify RazorTagHelper works + Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.TagHelpers.input.cache"); + Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.TagHelpers.output.cache"); + Assert.FileContains( + result, + Path.Combine(IntermediateOutputPath, "SimpleMvc.TagHelpers.output.cache"), + @"""Name"":""SimpleMvc.SimpleTagHelper"""); + + // Verify RazorGenerate works + Assert.FileCountEquals(result, 8, RazorIntermediateOutputPath, "*.cs"); } [Fact] @@ -40,13 +57,13 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests { var result = await DotnetMSBuild( "Build", - $"/p:RazorCompileOnBuild=true /p:UseRazorBuildServer=true /p:_RazorBuildServerPipeName={_pipeName} /p:_RazorForceBuildServer=true"); + "/p:_RazorForceBuildServer=true"); Assert.BuildPassed(result); Assert.FileExists(result, OutputPath, "SimpleMvc.dll"); Assert.FileExists(result, OutputPath, "SimpleMvc.pdb"); - Assert.FileExists(result, OutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileExists(result, OutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileExists(result, OutputPath, "SimpleMvc.Views.dll"); + Assert.FileExists(result, OutputPath, "SimpleMvc.Views.pdb"); } [Fact] @@ -55,15 +72,15 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests { var result = await DotnetMSBuild( "Build", - $"/p:RazorCompileOnBuild=true /p:UseRazorBuildServer=true /p:_RazorBuildServerPipeName={_pipeName} /p:_RazorForceBuildServer=true"); + "/p:_RazorForceBuildServer=true"); Assert.BuildPassed(result); Assert.FileExists(result, OutputPath, "Whitespace in name.dll"); Assert.FileExists(result, OutputPath, "Whitespace in name.pdb"); - Assert.FileExists(result, OutputPath, "Whitespace in name.PrecompiledViews.dll"); - Assert.FileExists(result, OutputPath, "Whitespace in name.PrecompiledViews.pdb"); + Assert.FileExists(result, OutputPath, "Whitespace in name.Views.dll"); + Assert.FileExists(result, OutputPath, "Whitespace in name.Views.pdb"); - Assert.FileExists(result, IntermediateOutputPath, "Whitespace in name.PrecompiledViews.dll"); + Assert.FileExists(result, IntermediateOutputPath, "Whitespace in name.Views.dll"); Assert.FileExists(result, IntermediateOutputPath, "Whitespace in name.RazorCoreGenerate.cache"); Assert.FileExists(result, RazorIntermediateOutputPath, "Views", "Home", "Index.cs"); } diff --git a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildServerTestFixture.cs b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildServerTestFixture.cs index f63de011a4..7f3933c0da 100644 --- a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildServerTestFixture.cs +++ b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildServerTestFixture.cs @@ -5,6 +5,8 @@ using System; using System.IO; using System.Threading; using Microsoft.AspNetCore.Razor.Tools; +using Microsoft.CodeAnalysis; +using Moq; namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests { @@ -29,16 +31,23 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests // Shutdown the build server. using (var cts = new CancellationTokenSource(_defaultShutdownTimeout)) { + var writer = new StringWriter(); + cts.Token.Register(() => { - throw new TimeoutException($"Shutting down the build server at pipe {PipeName} took longer than expected."); + var output = writer.ToString(); + throw new TimeoutException($"Shutting down the build server at pipe {PipeName} took longer than expected.{Environment.NewLine}Output: {output}."); }); - var application = new Application(cts.Token); + var application = new Application(cts.Token, Mock.Of(), Mock.Of(), (path, properties) => Mock.Of()) + { + Out = writer, + Error = writer, + }; var exitCode = application.Execute("shutdown", "-w", "-p", PipeName); if (exitCode != 0) { - var output = application.Error.ToString(); + var output = writer.ToString(); throw new InvalidOperationException( $"Build server at pipe {PipeName} failed to shutdown with exit code {exitCode}. Output: {output}"); } diff --git a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/ConfigurationMetadataIntegrationTest.cs b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/ConfigurationMetadataIntegrationTest.cs index f527157a89..b4f81e102e 100644 --- a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/ConfigurationMetadataIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/ConfigurationMetadataIntegrationTest.cs @@ -7,18 +7,23 @@ using Xunit; namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests { - public class ConfigurationMetadataIntegrationTest : MSBuildIntegrationTestBase + public class ConfigurationMetadataIntegrationTest : MSBuildIntegrationTestBase, IClassFixture { + public ConfigurationMetadataIntegrationTest(BuildServerTestFixture buildServer) + : base(buildServer) + { + } + [Fact] [InitializeTestProject("SimpleMvc")] public async Task Build_WithMvc_AddsConfigurationMetadata() { - var result = await DotnetMSBuild("Build", $"/p:RazorCompileOnBuild=true"); + var result = await DotnetMSBuild("Build"); Assert.BuildPassed(result); - Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.Views.dll"); + Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.Views.pdb"); Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.AssemblyInfo.cs"); Assert.FileContainsLine( @@ -39,12 +44,12 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests [InitializeTestProject("SimpleMvc")] public async Task Build_WithGenerateRazorAssemblyInfo_False_SuppressesConfigurationMetadata() { - var result = await DotnetMSBuild("Build", $"/p:RazorCompileOnBuild=true /p:GenerateRazorAssemblyInfo=false"); + var result = await DotnetMSBuild("Build", "/p:GenerateRazorAssemblyInfo=false"); Assert.BuildPassed(result); - Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.Views.dll"); + Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.Views.pdb"); Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.AssemblyInfo.cs"); Assert.FileDoesNotContainLine( @@ -65,12 +70,14 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests [InitializeTestProject("ClassLibrary")] public async Task Build_ForClassLibrary_SuppressesConfigurationMetadata() { - var result = await DotnetMSBuild("Build", $"/p:RazorCompileOnBuild=true"); + TargetFramework = "netstandard2.0"; + + var result = await DotnetMSBuild("Build"); Assert.BuildPassed(result); - Assert.FileExists(result, IntermediateOutputPath, "ClassLibrary.PrecompiledViews.dll"); - Assert.FileExists(result, IntermediateOutputPath, "ClassLibrary.PrecompiledViews.pdb"); + Assert.FileExists(result, IntermediateOutputPath, "ClassLibrary.Views.dll"); + Assert.FileExists(result, IntermediateOutputPath, "ClassLibrary.Views.pdb"); Assert.FileExists(result, IntermediateOutputPath, "ClassLibrary.AssemblyInfo.cs"); Assert.FileDoesNotContainLine( diff --git a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/DesignTimeBuildIntegrationTest.cs b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/DesignTimeBuildIntegrationTest.cs index 6480149992..9fd41950e7 100644 --- a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/DesignTimeBuildIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/DesignTimeBuildIntegrationTest.cs @@ -6,21 +6,26 @@ using Xunit; namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests { - public class DesignTimeBuildIntegrationTest : MSBuildIntegrationTestBase + public class DesignTimeBuildIntegrationTest : MSBuildIntegrationTestBase, IClassFixture { + public DesignTimeBuildIntegrationTest(BuildServerTestFixture buildServer) + : base(buildServer) + { + } + [Fact] [InitializeTestProject("SimpleMvc")] public async Task DesignTimeBuild_DoesNotRunRazorTargets() { // Using Compile here instead of CompileDesignTime because the latter is only defined when using // the VS targets. This is a close enough simulation for an SDK project - var result = await DotnetMSBuild("Compile", "/p:RazorCompileOnBuild=true /p:DesignTimeBuild=true /clp:PerformanceSummary"); + var result = await DotnetMSBuild("Compile", "/p:DesignTimeBuild=true /clp:PerformanceSummary"); Assert.BuildPassed(result); Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.dll"); Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.pdb"); - Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.Views.dll"); + Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.Views.pdb"); // This target should be part of the design time build. Assert.Contains("RazorGetAssemblyAttributes", result.Output); diff --git a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/MSBuildIntegrationTestBase.cs b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/MSBuildIntegrationTestBase.cs index b69109869b..5247f1aca9 100644 --- a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/MSBuildIntegrationTestBase.cs +++ b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/MSBuildIntegrationTestBase.cs @@ -15,8 +15,9 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests { private static readonly AsyncLocal _project = new AsyncLocal(); - protected MSBuildIntegrationTestBase() + protected MSBuildIntegrationTestBase(BuildServerTestFixture buildServer) { + BuildServer = buildServer; } #if DEBUG @@ -44,19 +45,35 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests protected string TargetFramework { get; set; } = "netcoreapp2.0"; + protected BuildServerTestFixture BuildServer { get; set; } + internal Task DotnetMSBuild( string target, string args = null, bool suppressRestore = false, bool suppressTimeout = false, + bool suppressBuildServer = false, MSBuildProcessKind msBuildProcessKind = MSBuildProcessKind.Dotnet) { var timeout = suppressTimeout ? (TimeSpan?)Timeout.InfiniteTimeSpan : null; - var restoreArgument = suppressRestore ? "" : "/restore"; + var buildArgumentList = new List(); + + if (!suppressRestore) + { + buildArgumentList.Add("/restore"); + } + + if (!suppressBuildServer) + { + buildArgumentList.Add($"/p:_RazorBuildServerPipeName={BuildServer.PipeName}"); + } + + buildArgumentList.Add($"/t:{target} /p:Configuration={Configuration} {args}"); + var buildArguments = string.Join(" ", buildArgumentList); return MSBuildProcessManager.RunProcessAsync( Project, - $"{restoreArgument} /t:{target} /p:Configuration={Configuration} {args}", + buildArguments, timeout, msBuildProcessKind); } @@ -92,6 +109,9 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests } File.WriteAllText(filePath, content, Encoding.UTF8); + // Timestamps on xplat are precise only to a second. Update it's last write time by at least 1 second + // so we can ensure that MSBuild recognizes the file change. See https://github.com/dotnet/corefx/issues/26024 + File.SetLastWriteTimeUtc(filePath, File.GetLastWriteTimeUtc(filePath).AddSeconds(1)); } /// diff --git a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/PackIntegrationTest.cs b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/PackIntegrationTest.cs index d060fc02b8..d217a3b02d 100644 --- a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/PackIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/PackIntegrationTest.cs @@ -2,34 +2,120 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.IO; +using System.Runtime.InteropServices; using System.Threading.Tasks; using Xunit; namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests { - public class PackIntegrationTest : MSBuildIntegrationTestBase + public class PackIntegrationTest : MSBuildIntegrationTestBase, IClassFixture { + public PackIntegrationTest(BuildServerTestFixture buildServer) + : base(buildServer) + { + } + [Fact] [InitializeTestProject("ClassLibrary")] public async Task Pack_Works_IncludesRazorAssembly() { - var result = await DotnetMSBuild("Pack", "/p:RazorCompileOnBuild=true"); + TargetFramework = "netstandard2.0"; + var result = await DotnetMSBuild("Pack"); Assert.BuildPassed(result); Assert.FileExists(result, OutputPath, "ClassLibrary.dll"); - Assert.FileExists(result, OutputPath, "ClassLibrary.PrecompiledViews.dll"); + Assert.FileExists(result, OutputPath, "ClassLibrary.Views.dll"); - Assert.NuspecContains( + if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + // Travis on OSX produces different full paths in C# and MSBuild + Assert.NuspecContains( + result, + Path.Combine("obj", Configuration, "ClassLibrary.1.0.0.nuspec"), + $""); + + Assert.NuspecDoesNotContain( + result, + Path.Combine("obj", Configuration, "ClassLibrary.1.0.0.nuspec"), + $""); + } + + Assert.NuspecDoesNotContain( result, Path.Combine("obj", Configuration, "ClassLibrary.1.0.0.nuspec"), - $""); + @""); Assert.NupkgContains( result, Path.Combine("bin", Configuration, "ClassLibrary.1.0.0.nupkg"), - Path.Combine("lib", "netcoreapp2.0", "ClassLibrary.PrecompiledViews.dll")); + Path.Combine("lib", "netstandard2.0", "ClassLibrary.Views.dll")); + } + + [Fact] + [InitializeTestProject("ClassLibrary")] + public async Task Pack_WithIncludeSymbols_IncludesRazorPdb() + { + var result = await DotnetMSBuild("Pack", "/p:RazorCompileOnBuild=true /p:IncludeSymbols=true"); + + Assert.BuildPassed(result); + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + // Travis on OSX produces different full paths in C# and MSBuild + Assert.NuspecContains( + result, + Path.Combine("obj", Configuration, "ClassLibrary.1.0.0.symbols.nuspec"), + $""); + + Assert.NuspecContains( + result, + Path.Combine("obj", Configuration, "ClassLibrary.1.0.0.symbols.nuspec"), + $""); + } + + Assert.NupkgContains( + result, + Path.Combine("bin", Configuration, "ClassLibrary.1.0.0.symbols.nupkg"), + Path.Combine("lib", "netstandard2.0", "ClassLibrary.Views.dll"), + Path.Combine("lib", "netstandard2.0", "ClassLibrary.Views.pdb")); + } + + [Fact] + [InitializeTestProject("ClassLibrary")] + public async Task Pack_IncludesRazorFilesAsContent_WhenIncludeRazorContentInPack_IsSet() + { + TargetFramework = "netstandard2.0"; + var result = await DotnetMSBuild("Pack", "/p:IncludeRazorContentInPack=true"); + + Assert.BuildPassed(result); + + Assert.FileExists(result, OutputPath, "ClassLibrary.dll"); + Assert.FileExists(result, OutputPath, "ClassLibrary.Views.dll"); + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + // Travis on OSX produces different full paths in C# and MSBuild + Assert.NuspecContains( + result, + Path.Combine("obj", Configuration, "ClassLibrary.1.0.0.nuspec"), + $""); + + Assert.NuspecContains( + result, + Path.Combine("obj", Configuration, "ClassLibrary.1.0.0.nuspec"), + @""); + } + + Assert.NupkgContains( + result, + Path.Combine("bin", Configuration, "ClassLibrary.1.0.0.nupkg"), + Path.Combine("lib", "netstandard2.0", "ClassLibrary.Views.dll")); } } } diff --git a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/PublishIntegrationTest.cs b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/PublishIntegrationTest.cs index 6b80b97be6..22407311fa 100644 --- a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/PublishIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/PublishIntegrationTest.cs @@ -7,8 +7,13 @@ using Xunit; namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests { - public class PublishIntegrationTest : MSBuildIntegrationTestBase + public class PublishIntegrationTest : MSBuildIntegrationTestBase, IClassFixture { + public PublishIntegrationTest(BuildServerTestFixture buildServer) + : base(buildServer) + { + } + [Fact] [InitializeTestProject("SimpleMvc")] public async Task Publish_RazorCompileOnPublish_IsDefault() @@ -17,13 +22,10 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests Assert.BuildPassed(result); - Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.PrecompiledViews.pdb"); - Assert.FileExists(result, PublishOutputPath, "SimpleMvc.dll"); Assert.FileExists(result, PublishOutputPath, "SimpleMvc.pdb"); - Assert.FileExists(result, PublishOutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileExists(result, PublishOutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileExists(result, PublishOutputPath, "SimpleMvc.Views.dll"); + Assert.FileExists(result, PublishOutputPath, "SimpleMvc.Views.pdb"); // By default refs and .cshtml files will not be copied on publish Assert.FileCountEquals(result, 0, Path.Combine(PublishOutputPath, "refs"), "*.dll"); @@ -32,21 +34,21 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests [Fact] [InitializeTestProject("SimpleMvc")] - public async Task Publish_WithRazorCompileOnBuild_PublishesAssembly() + public async Task Publish_PublishesAssembly() { - var result = await DotnetMSBuild("Publish", "/p:RazorCompileOnBuild=true"); + var result = await DotnetMSBuild("Publish"); Assert.BuildPassed(result); Assert.FileExists(result, OutputPath, "SimpleMvc.dll"); Assert.FileExists(result, OutputPath, "SimpleMvc.pdb"); - Assert.FileExists(result, OutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileExists(result, OutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileExists(result, OutputPath, "SimpleMvc.Views.dll"); + Assert.FileExists(result, OutputPath, "SimpleMvc.Views.pdb"); Assert.FileExists(result, PublishOutputPath, "SimpleMvc.dll"); Assert.FileExists(result, PublishOutputPath, "SimpleMvc.pdb"); - Assert.FileExists(result, PublishOutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileExists(result, PublishOutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileExists(result, PublishOutputPath, "SimpleMvc.Views.dll"); + Assert.FileExists(result, PublishOutputPath, "SimpleMvc.Views.pdb"); // By default refs and .cshtml files will not be copied on publish Assert.FileCountEquals(result, 0, Path.Combine(PublishOutputPath, "refs"), "*.dll"); @@ -57,17 +59,36 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests [InitializeTestProject("SimpleMvc")] public async Task Publish_WithRazorCompileOnPublish_PublishesAssembly() { - var result = await DotnetMSBuild("Publish", "/p:RazorCompileOnPublish=true"); + var result = await DotnetMSBuild("Publish"); Assert.BuildPassed(result); - Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileExists(result, PublishOutputPath, "SimpleMvc.dll"); + Assert.FileExists(result, PublishOutputPath, "SimpleMvc.pdb"); + Assert.FileExists(result, PublishOutputPath, "SimpleMvc.Views.dll"); + Assert.FileExists(result, PublishOutputPath, "SimpleMvc.Views.pdb"); + + // By default refs and .cshtml files will not be copied on publish + Assert.FileCountEquals(result, 0, Path.Combine(PublishOutputPath, "refs"), "*.dll"); + Assert.FileCountEquals(result, 0, Path.Combine(PublishOutputPath, "Views"), "*.cshtml"); + } + + [Fact] + [InitializeTestProject("SimpleMvc")] + public async Task Publish_WithRazorCompileOnBuildFalse_PublishesAssembly() + { + // RazorCompileOnBuild is turned off, but RazorCompileOnPublish should still be enabled + var result = await DotnetMSBuild("Publish", "/p:RazorCompileOnBuild=false"); + + Assert.BuildPassed(result); + + Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.Views.dll"); + Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.Views.pdb"); Assert.FileExists(result, PublishOutputPath, "SimpleMvc.dll"); Assert.FileExists(result, PublishOutputPath, "SimpleMvc.pdb"); - Assert.FileExists(result, PublishOutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileExists(result, PublishOutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileExists(result, PublishOutputPath, "SimpleMvc.Views.dll"); + Assert.FileExists(result, PublishOutputPath, "SimpleMvc.Views.pdb"); // By default refs and .cshtml files will not be copied on publish Assert.FileCountEquals(result, 0, Path.Combine(PublishOutputPath, "refs"), "*.dll"); @@ -84,8 +105,8 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests Assert.FileExists(result, PublishOutputPath, "SimpleMvc.dll"); Assert.FileExists(result, PublishOutputPath, "SimpleMvc.pdb"); - Assert.FileDoesNotExist(result, PublishOutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileDoesNotExist(result, PublishOutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileDoesNotExist(result, PublishOutputPath, "SimpleMvc.Views.dll"); + Assert.FileDoesNotExist(result, PublishOutputPath, "SimpleMvc.Views.pdb"); } [Fact] // This is an override to force the new toolset @@ -96,13 +117,10 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests Assert.BuildPassed(result); - Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.PrecompiledViews.pdb"); - Assert.FileExists(result, PublishOutputPath, "SimpleMvc.dll"); Assert.FileExists(result, PublishOutputPath, "SimpleMvc.pdb"); - Assert.FileExists(result, PublishOutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileExists(result, PublishOutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileExists(result, PublishOutputPath, "SimpleMvc.Views.dll"); + Assert.FileExists(result, PublishOutputPath, "SimpleMvc.Views.pdb"); // By default refs and .cshtml files will not be copied on publish Assert.FileCountEquals(result, 0, Path.Combine(PublishOutputPath, "refs"), "*.dll"); @@ -115,15 +133,15 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests { Directory.Delete(Path.Combine(Project.DirectoryPath, "Views"), recursive: true); - var result = await DotnetMSBuild("Publish", "/p:RazorCompileOnBuild=true"); + var result = await DotnetMSBuild("Publish"); Assert.BuildPassed(result); // Everything we do should noop - including building the app. Assert.FileExists(result, PublishOutputPath, "SimpleMvc.dll"); Assert.FileExists(result, PublishOutputPath, "SimpleMvc.pdb"); - Assert.FileDoesNotExist(result, PublishOutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileDoesNotExist(result, PublishOutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileDoesNotExist(result, PublishOutputPath, "SimpleMvc.Views.dll"); + Assert.FileDoesNotExist(result, PublishOutputPath, "SimpleMvc.Views.pdb"); } [Fact] @@ -137,8 +155,8 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests // Everything we do should noop - including building the app. Assert.FileExists(result, PublishOutputPath, "SimpleMvc.dll"); Assert.FileExists(result, PublishOutputPath, "SimpleMvc.pdb"); - Assert.FileDoesNotExist(result, PublishOutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileDoesNotExist(result, PublishOutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileDoesNotExist(result, PublishOutputPath, "SimpleMvc.Views.dll"); + Assert.FileDoesNotExist(result, PublishOutputPath, "SimpleMvc.Views.pdb"); } [Fact] @@ -154,70 +172,67 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests // Everything we do should noop - including building the app. Assert.FileExists(result, PublishOutputPath, "SimpleMvc.dll"); Assert.FileExists(result, PublishOutputPath, "SimpleMvc.pdb"); - Assert.FileDoesNotExist(result, PublishOutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileDoesNotExist(result, PublishOutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileDoesNotExist(result, PublishOutputPath, "SimpleMvc.Views.dll"); + Assert.FileDoesNotExist(result, PublishOutputPath, "SimpleMvc.Views.pdb"); } [Fact] [InitializeTestProject("SimpleMvc")] public async Task Publish_SkipsCopyingBinariesToOutputDirectory_IfCopyBuildOutputToOutputDirectory_IsUnset() { - var result = await DotnetMSBuild("Publish", "/p:RazorCompileOnBuild=true /p:CopyBuildOutputToPublishDirectory=false"); + var result = await DotnetMSBuild("Publish", "/p:CopyBuildOutputToPublishDirectory=false"); Assert.BuildPassed(result); Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.dll"); - Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.PrecompiledViews.dll"); + Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.Views.dll"); Assert.FileDoesNotExist(result, PublishOutputPath, "SimpleMvc.dll"); - Assert.FileDoesNotExist(result, PublishOutputPath, "SimpleMvc.PrecompiledViews.dll"); + Assert.FileDoesNotExist(result, PublishOutputPath, "SimpleMvc.Views.dll"); } [Fact] [InitializeTestProject("SimpleMvc")] public async Task Publish_SkipsCopyingBinariesToOutputDirectory_IfCopyOutputSymbolsToOutputDirectory_IsUnset() { - var result = await DotnetMSBuild("Publish", "/p:RazorCompileOnBuild=true /p:CopyOutputSymbolsToPublishDirectory=false"); + var result = await DotnetMSBuild("Publish", "/p:CopyOutputSymbolsToPublishDirectory=false"); Assert.BuildPassed(result); - Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.Views.pdb"); - Assert.FileExists(result, PublishOutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileDoesNotExist(result, PublishOutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileExists(result, PublishOutputPath, "SimpleMvc.Views.dll"); + Assert.FileDoesNotExist(result, PublishOutputPath, "SimpleMvc.Views.pdb"); } [Fact] [InitializeTestProject("SimpleMvc")] public async Task Publish_Works_WhenSymbolsAreNotGenerated() { - var result = await DotnetMSBuild("Publish", "/p:RazorCompileOnBuild=true /p:DebugType=none"); + var result = await DotnetMSBuild("Publish", "/p:DebugType=none"); Assert.BuildPassed(result); Assert.FileDoesNotExist(result, IntermediateOutputPath, "SimpleMvc.pdb"); - Assert.FileDoesNotExist(result, IntermediateOutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileDoesNotExist(result, IntermediateOutputPath, "SimpleMvc.Views.pdb"); - Assert.FileExists(result, PublishOutputPath, "SimpleMvc.PrecompiledViews.dll"); + Assert.FileExists(result, PublishOutputPath, "SimpleMvc.Views.dll"); Assert.FileDoesNotExist(result, PublishOutputPath, "SimpleMvc.pdb"); - Assert.FileDoesNotExist(result, PublishOutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileDoesNotExist(result, PublishOutputPath, "SimpleMvc.Views.pdb"); } [Fact] [InitializeTestProject("SimpleMvc")] public async Task Publish_IncludeCshtmlAndRefAssemblies_CopiesFiles() { - var result = await DotnetMSBuild("Publish", "/p:RazorCompileOnPublish=true /p:CopyRazorGenerateFilesToPublishDirectory=true /p:CopyRefAssembliesToPublishDirectory=true"); + var result = await DotnetMSBuild("Publish", "/p:CopyRazorGenerateFilesToPublishDirectory=true /p:CopyRefAssembliesToPublishDirectory=true"); Assert.BuildPassed(result); - Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.PrecompiledViews.pdb"); - Assert.FileExists(result, PublishOutputPath, "SimpleMvc.dll"); Assert.FileExists(result, PublishOutputPath, "SimpleMvc.pdb"); - Assert.FileExists(result, PublishOutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileExists(result, PublishOutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileExists(result, PublishOutputPath, "SimpleMvc.Views.dll"); + Assert.FileExists(result, PublishOutputPath, "SimpleMvc.Views.pdb"); // By default refs and .cshtml files will not be copied on publish Assert.FileExists(result, PublishOutputPath, "refs", "mscorlib.dll"); @@ -228,17 +243,14 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests [InitializeTestProject("SimpleMvc")] public async Task Publish_MvcRazorExcludeFilesFromPublish_False_CopiesFiles() { - var result = await DotnetMSBuild("Publish", "/p:RazorCompileOnPublish=true /p:MvcRazorExcludeViewFilesFromPublish=false /p:MvcRazorExcludeRefAssembliesFromPublish=false"); + var result = await DotnetMSBuild("Publish", "/p:MvcRazorExcludeViewFilesFromPublish=false /p:MvcRazorExcludeRefAssembliesFromPublish=false"); Assert.BuildPassed(result); - Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileDoesNotExist(result, OutputPath, "SimpleMvc.PrecompiledViews.pdb"); - Assert.FileExists(result, PublishOutputPath, "SimpleMvc.dll"); Assert.FileExists(result, PublishOutputPath, "SimpleMvc.pdb"); - Assert.FileExists(result, PublishOutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileExists(result, PublishOutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileExists(result, PublishOutputPath, "SimpleMvc.Views.dll"); + Assert.FileExists(result, PublishOutputPath, "SimpleMvc.Views.pdb"); // By default refs and .cshtml files will not be copied on publish Assert.FileExists(result, PublishOutputPath, "refs", "mscorlib.dll"); @@ -249,41 +261,36 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests [InitializeTestProject("AppWithP2PReference", "ClassLibrary")] public async Task Publish_WithP2P_AndRazorCompileOnBuild_CopiesRazorAssembly() { - var result = await DotnetMSBuild("Publish", "/p:RazorCompileOnBuild=true"); + var result = await DotnetMSBuild("Publish"); Assert.BuildPassed(result); Assert.FileExists(result, PublishOutputPath, "AppWithP2PReference.dll"); Assert.FileExists(result, PublishOutputPath, "AppWithP2PReference.pdb"); - Assert.FileExists(result, PublishOutputPath, "AppWithP2PReference.PrecompiledViews.dll"); - Assert.FileExists(result, PublishOutputPath, "AppWithP2PReference.PrecompiledViews.pdb"); + Assert.FileExists(result, PublishOutputPath, "AppWithP2PReference.Views.dll"); + Assert.FileExists(result, PublishOutputPath, "AppWithP2PReference.Views.pdb"); Assert.FileExists(result, PublishOutputPath, "ClassLibrary.dll"); Assert.FileExists(result, PublishOutputPath, "ClassLibrary.pdb"); - Assert.FileExists(result, PublishOutputPath, "ClassLibrary.PrecompiledViews.dll"); - Assert.FileExists(result, PublishOutputPath, "ClassLibrary.PrecompiledViews.pdb"); + Assert.FileExists(result, PublishOutputPath, "ClassLibrary.Views.dll"); + Assert.FileExists(result, PublishOutputPath, "ClassLibrary.Views.pdb"); } [Fact] [InitializeTestProject("AppWithP2PReference", "ClassLibrary")] public async Task Publish_WithP2P_AndRazorCompileOnPublish_CopiesRazorAssembly() { - var result = await DotnetMSBuild("Publish", "/p:RazorCompileOnPublish=true"); + var result = await DotnetMSBuild("Publish"); Assert.BuildPassed(result); - Assert.FileDoesNotExist(result, OutputPath, "AppWithP2PReference.PrecompiledViews.dll"); - Assert.FileDoesNotExist(result, OutputPath, "AppWithP2PReference.PrecompiledViews.pdb"); - Assert.FileDoesNotExist(result, OutputPath, "ClassLibrary.PrecompiledViews.dll"); - Assert.FileDoesNotExist(result, OutputPath, "ClassLibrary.PrecompiledViews.pdb"); - Assert.FileExists(result, PublishOutputPath, "AppWithP2PReference.dll"); Assert.FileExists(result, PublishOutputPath, "AppWithP2PReference.pdb"); - Assert.FileExists(result, PublishOutputPath, "AppWithP2PReference.PrecompiledViews.dll"); - Assert.FileExists(result, PublishOutputPath, "AppWithP2PReference.PrecompiledViews.pdb"); + Assert.FileExists(result, PublishOutputPath, "AppWithP2PReference.Views.dll"); + Assert.FileExists(result, PublishOutputPath, "AppWithP2PReference.Views.pdb"); Assert.FileExists(result, PublishOutputPath, "ClassLibrary.dll"); Assert.FileExists(result, PublishOutputPath, "ClassLibrary.pdb"); - Assert.FileExists(result, PublishOutputPath, "ClassLibrary.PrecompiledViews.dll"); - Assert.FileExists(result, PublishOutputPath, "ClassLibrary.PrecompiledViews.pdb"); + Assert.FileExists(result, PublishOutputPath, "ClassLibrary.Views.dll"); + Assert.FileExists(result, PublishOutputPath, "ClassLibrary.Views.pdb"); } } } diff --git a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/RazorCompileIntegrationTest.cs b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/RazorCompileIntegrationTest.cs index 08d763ea7d..c375b99354 100644 --- a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/RazorCompileIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/RazorCompileIntegrationTest.cs @@ -9,8 +9,13 @@ using Xunit; namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests { - public class RazorCompileIntegrationTest : MSBuildIntegrationTestBase + public class RazorCompileIntegrationTest : MSBuildIntegrationTestBase, IClassFixture { + public RazorCompileIntegrationTest(BuildServerTestFixture buildServer) + : base(buildServer) + { + } + [Fact] [InitializeTestProject("SimpleMvc")] public async Task RazorCompile_Success_CompilesAssembly() @@ -22,8 +27,8 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests // RazorGenerate should compile the assembly and pdb. Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.dll"); Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.pdb"); - Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.Views.dll"); + Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.Views.pdb"); } [Fact] @@ -39,8 +44,8 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests // Everything we do should noop - including building the app. Assert.FileDoesNotExist(result, IntermediateOutputPath, "SimpleMvc.dll"); Assert.FileDoesNotExist(result, IntermediateOutputPath, "SimpleMvc.pdb"); - Assert.FileDoesNotExist(result, IntermediateOutputPath, "SimpleMvc.PrecompiledViews.dll"); - Assert.FileDoesNotExist(result, IntermediateOutputPath, "SimpleMvc.PrecompiledViews.pdb"); + Assert.FileDoesNotExist(result, IntermediateOutputPath, "SimpleMvc.Views.dll"); + Assert.FileDoesNotExist(result, IntermediateOutputPath, "SimpleMvc.Views.pdb"); } [Fact] @@ -51,9 +56,9 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests Assert.BuildPassed(result); - Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.PrecompiledViews.dll"); + Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.Views.dll"); - var assembly = LoadAssemblyFromBytes(result.Project.DirectoryPath, IntermediateOutputPath, "SimpleMvc.PrecompiledViews.dll"); + var assembly = LoadAssemblyFromBytes(result.Project.DirectoryPath, IntermediateOutputPath, "SimpleMvc.Views.dll"); var resources = assembly.GetManifestResourceNames(); Assert.Equal(new string[] @@ -78,9 +83,9 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests Assert.BuildPassed(result); - Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.PrecompiledViews.dll"); + Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.Views.dll"); - var assembly = LoadAssemblyFromBytes(result.Project.DirectoryPath, IntermediateOutputPath, "SimpleMvc.PrecompiledViews.dll"); + var assembly = LoadAssemblyFromBytes(result.Project.DirectoryPath, IntermediateOutputPath, "SimpleMvc.Views.dll"); var resources = assembly.GetManifestResourceNames(); Assert.Equal(new string[] diff --git a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/RazorGenerateIntegrationTest.cs b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/RazorGenerateIntegrationTest.cs index bbee0e3ff4..0834a37974 100644 --- a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/RazorGenerateIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/RazorGenerateIntegrationTest.cs @@ -10,10 +10,15 @@ using Xunit; namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests { - public class RazorGenerateIntegrationTest : MSBuildIntegrationTestBase + public class RazorGenerateIntegrationTest : MSBuildIntegrationTestBase, IClassFixture { private const string RazorGenerateTarget = "RazorGenerate"; + public RazorGenerateIntegrationTest(BuildServerTestFixture buildServer) + : base(buildServer) + { + } + [Fact] [InitializeTestProject("SimpleMvc")] public async Task RazorGenerate_Success_GeneratesFilesOnDisk() @@ -24,7 +29,15 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests // RazorGenerate should compile the assembly, but not the views. Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.dll"); - Assert.FileDoesNotExist(result, IntermediateOutputPath, "SimpleMvc.PrecompiledViews.dll"); + Assert.FileDoesNotExist(result, IntermediateOutputPath, "SimpleMvc.Views.dll"); + + // RazorGenerate should generate correct TagHelper caches + Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.TagHelpers.input.cache"); + Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.TagHelpers.output.cache"); + Assert.FileContains( + result, + Path.Combine(IntermediateOutputPath, "SimpleMvc.TagHelpers.output.cache"), + @"""Name"":""SimpleMvc.SimpleTagHelper"""); Assert.FileExists(result, RazorIntermediateOutputPath, "Views", "_ViewImports.cs"); Assert.FileExists(result, RazorIntermediateOutputPath, "Views", "_ViewStart.cs"); @@ -44,7 +57,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests // Introducing a syntax error, an unclosed brace ReplaceContent("@{", "Views", "Home", "Index.cshtml"); - var result = await DotnetMSBuild("RazorGenerate"); + var result = await DotnetMSBuild(RazorGenerateTarget); Assert.BuildFailed(result); @@ -53,7 +66,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests // RazorGenerate should compile the assembly, but not the views. Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.dll"); - Assert.FileDoesNotExist(result, IntermediateOutputPath, "SimpleMvc.PrecompiledViews.dll"); + Assert.FileDoesNotExist(result, IntermediateOutputPath, "SimpleMvc.Views.dll"); // The file should still be generated even if we had a Razor syntax error. Assert.FileExists(result, RazorIntermediateOutputPath, "Views", "Home", "Index.cs"); @@ -101,9 +114,6 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests // Act - 2 // Update the source content and build. We should expect the outputs to be regenerated. - // Timestamps on xplat are precise only to a second. Add a delay so we can ensure that MSBuild recognizes the - // file change. See https://github.com/dotnet/corefx/issues/26024 - await Task.Delay(TimeSpan.FromSeconds(1)); ReplaceContent("Uodated content", file); result = await DotnetMSBuild(RazorGenerateTarget); @@ -313,7 +323,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests // RazorGenerate should compile the assembly, but not the views. Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.dll"); - Assert.FileDoesNotExist(result, IntermediateOutputPath, "SimpleMvc.PrecompiledViews.dll"); + Assert.FileDoesNotExist(result, IntermediateOutputPath, "SimpleMvc.Views.dll"); Assert.FileExists(result, RazorIntermediateOutputPath, "Views", "_ViewImports.cs"); Assert.FileExists(result, RazorIntermediateOutputPath, "Views", "_ViewStart.cs"); diff --git a/test/Microsoft.AspNetCore.Razor.Design.Test/Microsoft.AspNetCore.Razor.Design.Test.csproj b/test/Microsoft.AspNetCore.Razor.Design.Test/Microsoft.AspNetCore.Razor.Design.Test.csproj index 4e8c1104ee..9f94326610 100644 --- a/test/Microsoft.AspNetCore.Razor.Design.Test/Microsoft.AspNetCore.Razor.Design.Test.csproj +++ b/test/Microsoft.AspNetCore.Razor.Design.Test/Microsoft.AspNetCore.Razor.Design.Test.csproj @@ -34,7 +34,6 @@ - @@ -43,6 +42,7 @@ false + diff --git a/test/Microsoft.AspNetCore.Razor.Design.Test/xunit.runner.json b/test/Microsoft.AspNetCore.Razor.Design.Test/xunit.runner.json index fcf172c8fc..d00e4ae907 100644 --- a/test/Microsoft.AspNetCore.Razor.Design.Test/xunit.runner.json +++ b/test/Microsoft.AspNetCore.Razor.Design.Test/xunit.runner.json @@ -1,4 +1,5 @@ { "methodDisplay": "method", - "shadowCopy": false + "shadowCopy": false, + "maxParallelThreads": -1 } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/CodeGeneration/DefaultDocumentWriterTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/CodeGeneration/DefaultDocumentWriterTest.cs index 99678f74b6..67b03f39b8 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/CodeGeneration/DefaultDocumentWriterTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/CodeGeneration/DefaultDocumentWriterTest.cs @@ -260,6 +260,24 @@ internal class TestClass : TestBase, IFoo, IBar "async", }, MethodName = "TestMethod", + Parameters = + { + new MethodParameter() + { + Modifiers = + { + "readonly", + "ref", + }, + ParameterName = "a", + TypeName = "int", + }, + new MethodParameter() + { + ParameterName = "b", + TypeName = "string", + } + }, ReturnType = "string", }); @@ -279,7 +297,7 @@ internal class TestClass : TestBase, IFoo, IBar // #pragma warning disable 1591 #pragma warning disable 1998 -internal virtual async string TestMethod() +internal virtual async string TestMethod(readonly ref int a, string b) { } #pragma warning restore 1998 diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorProjectEngineIntegrationTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorProjectEngineIntegrationTest.cs index e767978411..38b5082b2e 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorProjectEngineIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorProjectEngineIntegrationTest.cs @@ -1,6 +1,7 @@ // 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 Moq; using Xunit; @@ -8,16 +9,61 @@ namespace Microsoft.AspNetCore.Razor.Language { public class DefaultRazorProjectEngineIntegrationTest { + [Fact] + public void Process_SetsOptions_Runtime() + { + // Arrange + var projectItem = new TestRazorProjectItem("Index.cshtml"); + + var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, TestRazorProjectFileSystem.Empty); + + // Act + var codeDocument = projectEngine.Process(projectItem); + + // Assert + var parserOptions = codeDocument.GetParserOptions(); + Assert.False(parserOptions.DesignTime); + + var codeGenerationOptions = codeDocument.GetCodeGenerationOptions(); + Assert.False(codeGenerationOptions.DesignTime); + Assert.False(codeGenerationOptions.SuppressChecksum); + Assert.False(codeGenerationOptions.SuppressMetadataAttributes); + } + + [Fact] + public void ProcessDesignTime_SetsOptions_DesignTime() + { + // Arrange + var projectItem = new TestRazorProjectItem("Index.cshtml"); + + var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, TestRazorProjectFileSystem.Empty); + + // Act + var codeDocument = projectEngine.ProcessDesignTime(projectItem); + + // Assert + var parserOptions = codeDocument.GetParserOptions(); + Assert.True(parserOptions.DesignTime); + + var codeGenerationOptions = codeDocument.GetCodeGenerationOptions(); + Assert.True(codeGenerationOptions.DesignTime); + Assert.True(codeGenerationOptions.SuppressChecksum); + Assert.True(codeGenerationOptions.SuppressMetadataAttributes); + } + [Fact] public void Process_GetsImportsFromFeature() { // Arrange var projectItem = new TestRazorProjectItem("Index.cshtml"); - var testImport = TestRazorSourceDocument.Create(); - var importFeature = new Mock(); - importFeature.Setup(feature => feature.GetImports(It.IsAny())) + + var testImport = Mock.Of(i => i.Read() == new MemoryStream() && i.FilePath == "testvalue" && i.Exists == true); + var importFeature = new Mock(); + importFeature + .Setup(feature => feature.GetImports(It.IsAny())) .Returns(new[] { testImport }); - var projectEngine = RazorProjectEngine.Create(TestRazorProjectFileSystem.Empty, builder => + + var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, TestRazorProjectFileSystem.Empty, builder => { builder.SetImportFeature(importFeature.Object); }); @@ -27,7 +73,7 @@ namespace Microsoft.AspNetCore.Razor.Language // Assert var import = Assert.Single(codeDocument.Imports); - Assert.Same(testImport, import); + Assert.Equal("testvalue", import.FilePath); } [Fact] @@ -35,7 +81,7 @@ namespace Microsoft.AspNetCore.Razor.Language { // Arrange var projectItem = new TestRazorProjectItem("Index.cshtml"); - var projectEngine = RazorProjectEngine.Create(TestRazorProjectFileSystem.Empty); + var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, TestRazorProjectFileSystem.Empty); // Act var codeDocument = projectEngine.Process(projectItem); diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorProjectEngineTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorProjectEngineTest.cs new file mode 100644 index 0000000000..2ca4da9881 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorProjectEngineTest.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 Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Language +{ + public class DefaultRazorProjectEngineTest + { + [Fact] + public void GetImportSourceDocuments_DoesNotIncludeNonExistentItems() + { + // Arrange + var existingItem = new TestRazorProjectItem("Index.cshtml"); + var nonExistentItem = Mock.Of(item => item.Exists == false); + var items = new[] { existingItem, nonExistentItem }; + + // Act + var sourceDocuments = DefaultRazorProjectEngine.GetImportSourceDocuments(items); + + // Assert + var sourceDocument = Assert.Single(sourceDocuments); + Assert.Equal(existingItem.FilePath, sourceDocument.FilePath); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorProjectFileSystemTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorProjectFileSystemTest.cs index addc55ebea..af48d1e900 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorProjectFileSystemTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorProjectFileSystemTest.cs @@ -143,7 +143,7 @@ namespace Microsoft.AspNetCore.Razor.Language Assert.Equal("/_ViewImports.cshtml", item.FilePath); Assert.Equal("/Views", item.BasePath); Assert.Equal(Path.Combine(TestFolder, "Views", "_ViewImports.cshtml"), item.PhysicalPath); - Assert.Equal(Path.Combine( "_ViewImports.cshtml"), item.RelativePhysicalPath); + Assert.Equal(Path.Combine("_ViewImports.cshtml"), item.RelativePhysicalPath); }, item => { diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Extensions/DefaultTagHelperTargetExtensionTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Extensions/DefaultTagHelperTargetExtensionTest.cs index 67084a4052..dcb8d6d6ff 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/Extensions/DefaultTagHelperTargetExtensionTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Extensions/DefaultTagHelperTargetExtensionTest.cs @@ -68,7 +68,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions public void WriteTagHelperBody_DesignTime_WritesChildren() { // Arrange - var extension = new DefaultTagHelperTargetExtension() { DesignTime = true }; + var extension = new DefaultTagHelperTargetExtension(); var context = TestCodeRenderingContext.CreateDesignTime(); var tagHelperNode = new TagHelperIntermediateNode(); @@ -133,7 +133,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions public void WriteTagHelperCreate_DesignTime_RendersCorrectly_UsesSpecifiedTagHelperType() { // Arrange - var extension = new DefaultTagHelperTargetExtension() { DesignTime = true }; + var extension = new DefaultTagHelperTargetExtension(); var context = TestCodeRenderingContext.CreateDesignTime(); var tagHelperNode = new TagHelperIntermediateNode(); @@ -190,7 +190,7 @@ __tagHelperExecutionContext.Add(__TestNamespace_MyTagHelper); public void WriteTagHelperExecute_DesignTime_WritesNothing() { // Arrange - var extension = new DefaultTagHelperTargetExtension() { DesignTime = true }; + var extension = new DefaultTagHelperTargetExtension(); var context = TestCodeRenderingContext.CreateDesignTime(); var tagHelperNode = new TagHelperIntermediateNode(); @@ -243,7 +243,7 @@ __tagHelperExecutionContext = __tagHelperScopeManager.End(); public void WriteTagHelperHtmlAttribute_DesignTime_WritesNothing() { // Arrange - var extension = new DefaultTagHelperTargetExtension() { DesignTime = true }; + var extension = new DefaultTagHelperTargetExtension(); var context = TestCodeRenderingContext.CreateDesignTime(); var tagHelperNode = new TagHelperIntermediateNode(); @@ -431,7 +431,7 @@ EndAddHtmlAttributeValues(__tagHelperExecutionContext); public void WriteTagHelperProperty_DesignTime_StringProperty_HtmlContent_RendersCorrectly() { // Arrange - var extension = new DefaultTagHelperTargetExtension() { DesignTime = true }; + var extension = new DefaultTagHelperTargetExtension(); var context = TestCodeRenderingContext.CreateDesignTime(); var tagHelperNode = new TagHelperIntermediateNode(); @@ -472,7 +472,7 @@ __InputTagHelper.StringProp = ""value""; public void WriteTagHelperProperty_DesignTime_StringProperty_NonHtmlContent_RendersCorrectly() { // Arrange - var extension = new DefaultTagHelperTargetExtension() { DesignTime = true }; + var extension = new DefaultTagHelperTargetExtension(); var context = TestCodeRenderingContext.CreateDesignTime(); var tagHelperNode = new TagHelperIntermediateNode(); @@ -513,7 +513,7 @@ __InputTagHelper.StringProp = string.Empty; public void WriteTagHelperProperty_DesignTime_NonStringProperty_RendersCorrectly() { // Arrange - var extension = new DefaultTagHelperTargetExtension() { DesignTime = true }; + var extension = new DefaultTagHelperTargetExtension(); var context = TestCodeRenderingContext.CreateDesignTime(); var tagHelperNode = new TagHelperIntermediateNode(); @@ -560,7 +560,7 @@ __InputTagHelper.IntProp = 32; public void WriteTagHelperProperty_DesignTime_NonStringProperty_SecondUseOfAttribute() { // Arrange - var extension = new DefaultTagHelperTargetExtension() { DesignTime = true }; + var extension = new DefaultTagHelperTargetExtension(); var context = TestCodeRenderingContext.CreateDesignTime(); var tagHelperNode = new TagHelperIntermediateNode(); @@ -602,7 +602,7 @@ __InputTagHelper.IntProp = 32; public void WriteTagHelperProperty_DesignTime_NonStringProperty_RendersCorrectly_WithoutLocation() { // Arrange - var extension = new DefaultTagHelperTargetExtension() { DesignTime = true }; + var extension = new DefaultTagHelperTargetExtension(); var context = TestCodeRenderingContext.CreateDesignTime(); var tagHelperNode = new TagHelperIntermediateNode(); @@ -642,7 +642,7 @@ __InputTagHelper.IntProp = 32; public void WriteTagHelperProperty_DesignTime_NonStringIndexer_RendersCorrectly() { // Arrange - var extension = new DefaultTagHelperTargetExtension() { DesignTime = true }; + var extension = new DefaultTagHelperTargetExtension(); var context = TestCodeRenderingContext.CreateDesignTime(); var tagHelperNode = new TagHelperIntermediateNode(); @@ -687,7 +687,7 @@ __InputTagHelper.IntIndexer[""bound""] = 32; public void WriteTagHelperProperty_DesignTime_NonStringIndexer_RendersCorrectly_WithoutLocation() { // Arrange - var extension = new DefaultTagHelperTargetExtension() { DesignTime = true }; + var extension = new DefaultTagHelperTargetExtension(); var context = TestCodeRenderingContext.CreateDesignTime(); var tagHelperNode = new TagHelperIntermediateNode(); @@ -1064,7 +1064,7 @@ __tagHelperExecutionContext.AddTagHelperAttribute(""foo-bound"", __InputTagHelpe public void WriteTagHelperRuntime_DesignTime_WritesNothing() { // Arrange - var extension = new DefaultTagHelperTargetExtension() { DesignTime = true }; + var extension = new DefaultTagHelperTargetExtension(); var context = TestCodeRenderingContext.CreateDesignTime(); var node = new DefaultTagHelperRuntimeIntermediateNode(); diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TokenizerLookaheadTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TokenizerLookaheadTest.cs index a9ac4be740..74d0c075f3 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TokenizerLookaheadTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TokenizerLookaheadTest.cs @@ -60,11 +60,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy public void LookaheadUntil_PassesThePreviousSymbolsInReverseOrder() { // Arrange - var source = TestRazorSourceDocument.Create("asdf--fvd--<"); - var options = RazorParserOptions.CreateDefault(); - var context = new ParserContext(source, options); - - var tokenizer = new TestTokenizerBackedParser(HtmlLanguageCharacteristics.Instance, context); + var tokenizer = CreateContentTokenizer("asdf--fvd--<"); // Act Stack symbols = new Stack(); @@ -86,11 +82,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy public void LookaheadUntil_ReturnsFalseAfterIteratingOverAllSymbolsIfConditionIsNotMet() { // Arrange - var source = TestRazorSourceDocument.Create("asdf--fvd"); - var options = RazorParserOptions.CreateDefault(); - var context = new ParserContext(source, options); - - var tokenizer = new TestTokenizerBackedParser(HtmlLanguageCharacteristics.Instance, context); + var tokenizer = CreateContentTokenizer("asdf--fvd"); // Act Stack symbols = new Stack(); @@ -112,11 +104,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy public void LookaheadUntil_ReturnsTrueAndBreaksIteration() { // Arrange - var source = TestRazorSourceDocument.Create("asdf--fvd"); - var options = RazorParserOptions.CreateDefault(); - var context = new ParserContext(source, options); - - var tokenizer = new TestTokenizerBackedParser(HtmlLanguageCharacteristics.Instance, context); + var tokenizer = CreateContentTokenizer("asdf--fvd"); // Act Stack symbols = new Stack(); @@ -133,6 +121,16 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy Assert.Equal(new HtmlSymbol("asdf", HtmlSymbolType.Text), symbols.Pop()); } + private static TestTokenizerBackedParser CreateContentTokenizer(string content) + { + var source = TestRazorSourceDocument.Create(content); + var options = RazorParserOptions.CreateDefault(); + var context = new ParserContext(source, options); + + var tokenizer = new TestTokenizerBackedParser(HtmlLanguageCharacteristics.Instance, context); + return tokenizer; + } + private class ExposedTokenizer : Tokenizer { public ExposedTokenizer(string input) diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/RazorCodeDocumentExtensionsTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/RazorCodeDocumentExtensionsTest.cs index 14c6aa4453..41f899512b 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/RazorCodeDocumentExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/RazorCodeDocumentExtensionsTest.cs @@ -148,5 +148,67 @@ namespace Microsoft.AspNetCore.Razor.Language // Assert Assert.Same(expected, codeDocument.Items[typeof(TagHelperDocumentContext)]); } + + [Fact] + public void GetParserOptions_ReturnsSyntaxTree() + { + // Arrange + var codeDocument = TestRazorCodeDocument.CreateEmpty(); + + var expected = RazorParserOptions.CreateDefault(); + codeDocument.Items[typeof(RazorParserOptions)] = expected; + + // Act + var actual = codeDocument.GetParserOptions(); + + // Assert + Assert.Same(expected, actual); + } + + [Fact] + public void SetParserOptions_SetsSyntaxTree() + { + // Arrange + var codeDocument = TestRazorCodeDocument.CreateEmpty(); + + var expected = RazorParserOptions.CreateDefault(); + + // Act + codeDocument.SetParserOptions(expected); + + // Assert + Assert.Same(expected, codeDocument.Items[typeof(RazorParserOptions)]); + } + + [Fact] + public void GetCodeGenerationOptions_ReturnsSyntaxTree() + { + // Arrange + var codeDocument = TestRazorCodeDocument.CreateEmpty(); + + var expected = RazorCodeGenerationOptions.CreateDefault(); + codeDocument.Items[typeof(RazorCodeGenerationOptions)] = expected; + + // Act + var actual = codeDocument.GetCodeGenerationOptions(); + + // Assert + Assert.Same(expected, actual); + } + + [Fact] + public void SetCodeGenerationOptions_SetsSyntaxTree() + { + // Arrange + var codeDocument = TestRazorCodeDocument.CreateEmpty(); + + var expected = RazorCodeGenerationOptions.CreateDefault(); + + // Act + codeDocument.SetCodeGenerationOptions(expected); + + // Assert + Assert.Same(expected, codeDocument.Items[typeof(RazorCodeGenerationOptions)]); + } } } diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/RazorEngineTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/RazorEngineTest.cs index 50f9328b1b..424b5fddd4 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/RazorEngineTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/RazorEngineTest.cs @@ -145,7 +145,7 @@ namespace Microsoft.AspNetCore.Razor.Language Assert.Collection( feature.TargetExtensions, extension => Assert.IsType(extension), - extension => Assert.False(Assert.IsType(extension).DesignTime), + extension => Assert.IsType(extension), extension => Assert.IsType(extension)); } @@ -190,7 +190,7 @@ namespace Microsoft.AspNetCore.Razor.Language Assert.Collection( feature.TargetExtensions, extension => Assert.IsType(extension), - extension => Assert.True(Assert.IsType(extension).DesignTime), + extension => Assert.IsType(extension), extension => Assert.IsType(extension)); } diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/RazorProjectEngineBuilderExtensionsTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/RazorProjectEngineBuilderExtensionsTest.cs index b3e91c29bc..f0a57128c3 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/RazorProjectEngineBuilderExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/RazorProjectEngineBuilderExtensionsTest.cs @@ -14,11 +14,11 @@ namespace Microsoft.AspNetCore.Razor.Language { // Arrange var builder = new DefaultRazorProjectEngineBuilder(RazorConfiguration.Default, Mock.Of()); - var testFeature1 = Mock.Of(); - var testFeature2 = Mock.Of(); + var testFeature1 = Mock.Of(); + var testFeature2 = Mock.Of(); builder.Features.Add(testFeature1); builder.Features.Add(testFeature2); - var newFeature = Mock.Of(); + var newFeature = Mock.Of(); // Act builder.SetImportFeature(newFeature); diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/RazorProjectEngineTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/RazorProjectEngineTest.cs new file mode 100644 index 0000000000..d901c16199 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/RazorProjectEngineTest.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.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Razor.Language.Extensions; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Language.Test +{ + public class RazorProjectEngineTest + { + [Fact] + public void CreateDesignTime_Lambda_AddsFeaturesAndPhases() + { + // Arrange + + // Act + var engine = RazorProjectEngine.Create(RazorConfiguration.Default, Mock.Of()); + + // Assert + AssertDefaultPhases(engine); + AssertDefaultFeatures(engine); + AssertDefaultDirectives(engine); + AssertDefaultTargetExtensions(engine); + } + + private static void AssertDefaultPhases(RazorProjectEngine engine) + { + Assert.Collection( + engine.Phases, + phase => Assert.IsType(phase), + phase => Assert.IsType(phase), + phase => Assert.IsType(phase), + phase => Assert.IsType(phase), + phase => Assert.IsType(phase), + phase => Assert.IsType(phase), + phase => Assert.IsType(phase), + phase => Assert.IsType(phase)); + } + + private static void AssertDefaultFeatures(RazorProjectEngine engine) + { + var features = engine.EngineFeatures.OrderBy(f => f.GetType().Name).ToArray(); + Assert.Collection( + features, + feature => Assert.IsType(feature), + feature => Assert.IsType(feature), + feature => Assert.IsType(feature), + feature => Assert.IsType(feature), + feature => Assert.IsType(feature), + feature => Assert.IsType(feature), + feature => Assert.IsType(feature), + feature => Assert.IsType(feature), + feature => Assert.IsType(feature), + feature => Assert.IsType(feature), + feature => Assert.IsType(feature), + feature => Assert.IsType(feature), + feature => Assert.IsType(feature), + feature => Assert.IsType(feature)); + } + + private static void AssertDefaultDirectives(RazorProjectEngine engine) + { + var feature = engine.EngineFeatures.OfType().FirstOrDefault(); + Assert.NotNull(feature); + Assert.Empty(feature.Directives); + } + + private static void AssertDefaultTargetExtensions(RazorProjectEngine engine) + { + var feature = engine.EngineFeatures.OfType().FirstOrDefault(); + Assert.NotNull(feature); + + var extensions = feature.TargetExtensions.OrderBy(f => f.GetType().Name).ToArray(); + Assert.Collection( + extensions, + extension => Assert.IsType(extension), + extension => Assert.IsType(extension), + extension => Assert.IsType(extension), + extension => Assert.IsType(extension)); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Test.Common/Language/RazorProjectEngineBuilderExtensions.cs b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/RazorProjectEngineBuilderExtensions.cs new file mode 100644 index 0000000000..bcea84e918 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/RazorProjectEngineBuilderExtensions.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; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language.IntegrationTests; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language +{ + public static class RazorProjectEngineBuilderExtensions + { + public static RazorProjectEngineBuilder AddTagHelpers(this RazorProjectEngineBuilder builder, params TagHelperDescriptor[] tagHelpers) + { + return AddTagHelpers(builder, (IEnumerable)tagHelpers); + } + + public static RazorProjectEngineBuilder AddTagHelpers(this RazorProjectEngineBuilder builder, IEnumerable tagHelpers) + { + var feature = (TestTagHelperFeature)builder.Features.OfType().FirstOrDefault(); + if (feature == null) + { + feature = new TestTagHelperFeature(); + builder.Features.Add(feature); + } + + feature.TagHelpers.AddRange(tagHelpers); + return builder; + } + + public static RazorProjectEngineBuilder ConfigureDocumentClassifier(this RazorProjectEngineBuilder builder) + { + var feature = builder.Features.OfType().FirstOrDefault(); + if (feature == null) + { + feature = new DefaultDocumentClassifierPassFeature(); + builder.Features.Add(feature); + } + + feature.ConfigureNamespace.Clear(); + feature.ConfigureClass.Clear(); + feature.ConfigureMethod.Clear(); + + feature.ConfigureNamespace.Add((RazorCodeDocument codeDocument, NamespaceDeclarationIntermediateNode node) => + { + node.Content = "Microsoft.AspNetCore.Razor.Language.IntegrationTests.TestFiles"; + }); + + feature.ConfigureClass.Add((RazorCodeDocument codeDocument, ClassDeclarationIntermediateNode node) => + { + node.ClassName = IntegrationTestBase.FileName.Replace('/', '_'); + node.Modifiers.Clear(); + node.Modifiers.Add("public"); + }); + + feature.ConfigureMethod.Add((RazorCodeDocument codeDocument, MethodDeclarationIntermediateNode node) => + { + node.Modifiers.Clear(); + node.Modifiers.Add("public"); + node.Modifiers.Add("async"); + node.MethodName = "ExecuteAsync"; + node.ReturnType = typeof(Task).FullName; + }); + + return builder; + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorProjectFileSystem.cs b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorProjectFileSystem.cs index 08528b1014..f16fff0274 100644 --- a/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorProjectFileSystem.cs +++ b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorProjectFileSystem.cs @@ -4,13 +4,12 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; namespace Microsoft.AspNetCore.Razor.Language { - public class TestRazorProjectFileSystem : RazorProjectFileSystem + internal class TestRazorProjectFileSystem : DefaultRazorProjectFileSystem { - public static RazorProjectFileSystem Empty = new TestRazorProjectFileSystem(); + public new static RazorProjectFileSystem Empty = new TestRazorProjectFileSystem(); private readonly Dictionary _lookup; @@ -19,7 +18,7 @@ namespace Microsoft.AspNetCore.Razor.Language { } - public TestRazorProjectFileSystem(IList items) + public TestRazorProjectFileSystem(IList items) : base("/") { _lookup = items.ToDictionary(item => item.FilePath); } diff --git a/test/Microsoft.AspNetCore.Razor.Test.Common/Properties/AssemblyInfo.cs b/test/Microsoft.AspNetCore.Razor.Test.Common/Properties/AssemblyInfo.cs index 3a2cadef34..576c4e4e44 100644 --- a/test/Microsoft.AspNetCore.Razor.Test.Common/Properties/AssemblyInfo.cs +++ b/test/Microsoft.AspNetCore.Razor.Test.Common/Properties/AssemblyInfo.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Razor.Extensions.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Razor.Language.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.CodeAnalysis.Razor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.CodeAnalysis.Razor.Workspaces.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/test/Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib/Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib.csproj b/test/Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib/Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib.csproj new file mode 100644 index 0000000000..6872d97beb --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib/Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib.csproj @@ -0,0 +1,19 @@ + + + + netstandard2.0 + + + + + + + + + + + + + + + diff --git a/test/Microsoft.AspNetCore.Razor.Tools.Test/ConcurrentLruCacheTest.cs b/test/Microsoft.AspNetCore.Razor.Tools.Test/ConcurrentLruCacheTest.cs new file mode 100644 index 0000000000..77ad2f0cf4 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Tools.Test/ConcurrentLruCacheTest.cs @@ -0,0 +1,120 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + public class ConcurrentLruCacheTest + { + [Fact] + public void ConcurrentLruCache_HoldsCapacity() + { + // Arrange + var input = GetKeyValueArray(Enumerable.Range(1, 3)); + var expected = input.Reverse(); + + // Act + var cache = new ConcurrentLruCache(input); + + // Assert + Assert.Equal(expected, cache.TestingEnumerable); + } + + [Fact] + public void Add_ThrowsIfKeyExists() + { + // Arrange + var input = GetKeyValueArray(Enumerable.Range(1, 3)); + var cache = new ConcurrentLruCache(input); + + // Act & Assert + var exception = Assert.Throws(() => cache.Add(1, 1)); + Assert.StartsWith("Key already exists", exception.Message); + } + + [Fact] + public void GetOrAdd_AddsIfKeyDoesNotExist() + { + // Arrange + var input = GetKeyValueArray(Enumerable.Range(1, 3)); + var expected = GetKeyValueArray(Enumerable.Range(2, 3)).Reverse(); + var cache = new ConcurrentLruCache(input); + + // Act + cache.GetOrAdd(4, 4); + + // Assert + Assert.Equal(expected, cache.TestingEnumerable); + } + + [Fact] + public void Remove_RemovesEntry() + { + // Arrange + var input = GetKeyValueArray(Enumerable.Range(1, 3)); + var expected = GetKeyValueArray(Enumerable.Range(1, 2)).Reverse(); + var cache = new ConcurrentLruCache(input); + + // Act + var result = cache.Remove(3); + + // Assert + Assert.True(result); + Assert.Equal(expected, cache.TestingEnumerable); + } + + [Fact] + public void Remove_KeyNotFound_ReturnsFalse() + { + // Arrange + var input = GetKeyValueArray(Enumerable.Range(1, 3)); + var cache = new ConcurrentLruCache(input); + + // Act + var result = cache.Remove(4); + + // Assert + Assert.False(result); + } + + [Fact] + public void Add_NoRead_EvictsLastNode() + { + // Arrange + var input = GetKeyValueArray(Enumerable.Range(1, 3)); + var expected = GetKeyValueArray(Enumerable.Range(2, 3)).Reverse(); + var cache = new ConcurrentLruCache(input); + + // Act + cache.Add(4, 4); + + // Assert + Assert.Equal(expected, cache.TestingEnumerable); + } + + [Fact] + public void Add_ReadLastNode_EvictsSecondOldestNode() + { + // Arrange + var input = GetKeyValueArray(Enumerable.Range(1, 3)); + var expected = GetKeyValueArray(new int[] { 4, 1, 3 }); + var cache = new ConcurrentLruCache(input); + + // Act + cache.GetOrAdd(1, 1); // Read to make this MRU + cache.Add(4, 4); // Add a new node + + // Assert + Assert.Equal(expected, cache.TestingEnumerable); + } + + private KeyValuePair[] GetKeyValueArray(IEnumerable inputArray) + { + return inputArray.Select(v => new KeyValuePair(v, v)).ToArray(); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Tools.Test/DefaultExtensionAssemblyLoaderTest.cs b/test/Microsoft.AspNetCore.Razor.Tools.Test/DefaultExtensionAssemblyLoaderTest.cs new file mode 100644 index 0000000000..ad619e5570 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Tools.Test/DefaultExtensionAssemblyLoaderTest.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.IO; +using System.Text; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + public class DefaultExtensionAssemblyLoaderTest + { + [Fact] + public void LoadFromPath_CanLoadAssembly() + { + using (var directory = TempDirectory.Create()) + { + // Arrange + var alphaFilePath = LoaderTestResources.Alpha.WriteToFile(directory.DirectoryPath, "Alpha.dll"); + + var loader = new TestDefaultExtensionAssemblyLoader(Path.Combine(directory.DirectoryPath, "shadow")); + + // Act + var assembly = loader.LoadFromPath(alphaFilePath); + + // Assert + Assert.NotNull(assembly); + } + } + + [Fact] + public void LoadFromPath_DoesNotAddDuplicates_AfterLoadingByName() + { + using (var directory = TempDirectory.Create()) + { + // Arrange + var alphaFilePath = LoaderTestResources.Alpha.WriteToFile(directory.DirectoryPath, "Alpha.dll"); + var alphaFilePath2 = LoaderTestResources.Alpha.WriteToFile(directory.DirectoryPath, "Alpha2.dll"); + + var loader = new TestDefaultExtensionAssemblyLoader(Path.Combine(directory.DirectoryPath, "shadow")); + loader.AddAssemblyLocation(alphaFilePath); + + var assembly1 = loader.Load("Alpha"); + + // Act + var assembly2 = loader.LoadFromPath(alphaFilePath2); + + // Assert + Assert.Same(assembly1, assembly2); + } + } + + [Fact] + public void LoadFromPath_DoesNotAddDuplicates_AfterLoadingByPath() + { + using (var directory = TempDirectory.Create()) + { + // Arrange + var alphaFilePath = LoaderTestResources.Alpha.WriteToFile(directory.DirectoryPath, "Alpha.dll"); + var alphaFilePath2 = LoaderTestResources.Alpha.WriteToFile(directory.DirectoryPath, "Alpha2.dll"); + + var loader = new TestDefaultExtensionAssemblyLoader(Path.Combine(directory.DirectoryPath, "shadow")); + var assembly1 = loader.LoadFromPath(alphaFilePath); + + // Act + var assembly2 = loader.LoadFromPath(alphaFilePath2); + + // Assert + Assert.Same(assembly1, assembly2); + } + } + + [Fact] + public void Load_CanLoadAssemblyByName_AfterLoadingByPath() + { + using (var directory = TempDirectory.Create()) + { + // Arrange + var alphaFilePath = LoaderTestResources.Alpha.WriteToFile(directory.DirectoryPath, "Alpha.dll"); + + var loader = new TestDefaultExtensionAssemblyLoader(Path.Combine(directory.DirectoryPath, "shadow")); + var assembly1 = loader.LoadFromPath(alphaFilePath); + + // Act + var assembly2 = loader.Load(assembly1.FullName); + + // Assert + Assert.Same(assembly1, assembly2); + } + } + + [Fact] + public void LoadFromPath_WithDependencyPathsSpecified_CanLoadAssemblyDependencies() + { + using (var directory = TempDirectory.Create()) + { + // Arrange + var alphaFilePath = LoaderTestResources.Alpha.WriteToFile(directory.DirectoryPath, "Alpha.dll"); + var betaFilePath = LoaderTestResources.Beta.WriteToFile(directory.DirectoryPath, "Beta.dll"); + var gammaFilePath = LoaderTestResources.Gamma.WriteToFile(directory.DirectoryPath, "Gamma.dll"); + var deltaFilePath = LoaderTestResources.Delta.WriteToFile(directory.DirectoryPath, "Delta.dll"); + + var loader = new TestDefaultExtensionAssemblyLoader(Path.Combine(directory.DirectoryPath, "shadow")); + loader.AddAssemblyLocation(gammaFilePath); + loader.AddAssemblyLocation(deltaFilePath); + + // Act + var alpha = loader.LoadFromPath(alphaFilePath); + var beta = loader.LoadFromPath(betaFilePath); + + // Assert + var builder = new StringBuilder(); + + var a = alpha.CreateInstance("Alpha.A"); + a.GetType().GetMethod("Write").Invoke(a, new object[] { builder, "Test A" }); + + var b = beta.CreateInstance("Beta.B"); + b.GetType().GetMethod("Write").Invoke(b, new object[] { builder, "Test B" }); + var expected = @"Delta: Gamma: Alpha: Test A +Delta: Gamma: Beta: Test B +"; + + var actual = builder.ToString(); + + Assert.Equal(expected, actual); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Razor.Tools.Test/DefaultExtensionDependencyCheckerTest.cs b/test/Microsoft.AspNetCore.Razor.Tools.Test/DefaultExtensionDependencyCheckerTest.cs new file mode 100644 index 0000000000..b9b9c8ac11 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Tools.Test/DefaultExtensionDependencyCheckerTest.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.IO; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + public class DefaultExtensionDependencyCheckerTest + { + [Fact] + public void Check_ReturnsFalse_WithMissingDependency() + { + using (var directory = TempDirectory.Create()) + { + // Arrange + var output = new StringWriter(); + + var alphaFilePath = LoaderTestResources.Alpha.WriteToFile(directory.DirectoryPath, "Alpha.dll"); + + var loader = new TestDefaultExtensionAssemblyLoader(Path.Combine(directory.DirectoryPath, "shadow")); + var checker = new DefaultExtensionDependencyChecker(loader, output, output); + + // Act + var result = checker.Check(new[] { alphaFilePath, }); + + // Assert + Assert.False(result, "Check should not have passed: " + output.ToString()); + } + } + + [Fact] + public void Check_ReturnsTrue_WithAllDependenciesProvided() + { + using (var directory = TempDirectory.Create()) + { + // Arrange + var output = new StringWriter(); + + var alphaFilePath = LoaderTestResources.Alpha.WriteToFile(directory.DirectoryPath, "Alpha.dll"); + var betaFilePath = LoaderTestResources.Beta.WriteToFile(directory.DirectoryPath, "Beta.dll"); + var gammaFilePath = LoaderTestResources.Gamma.WriteToFile(directory.DirectoryPath, "Gamma.dll"); + var deltaFilePath = LoaderTestResources.Delta.WriteToFile(directory.DirectoryPath, "Delta.dll"); + + var loader = new TestDefaultExtensionAssemblyLoader(Path.Combine(directory.DirectoryPath, "shadow")); + var checker = new DefaultExtensionDependencyChecker(loader, output, output); + + // Act + var result = checker.Check(new[] { alphaFilePath, betaFilePath, gammaFilePath, deltaFilePath, }); + + // Assert + Assert.True(result, "Check should have passed: " + output.ToString()); + } + } + + [Fact] + public void Check_ReturnsFalse_WhenAssemblyHasDifferentMVID() + { + using (var directory = TempDirectory.Create()) + { + // Arrange + var output = new StringWriter(); + + // Load Beta.dll from the future Alpha.dll path to prime the assembly loader + var alphaFilePath = LoaderTestResources.Beta.WriteToFile(directory.DirectoryPath, "Alpha.dll"); + var betaFilePath = LoaderTestResources.Beta.WriteToFile(directory.DirectoryPath, "Beta.dll"); + var gammaFilePath = LoaderTestResources.Gamma.WriteToFile(directory.DirectoryPath, "Gamma.dll"); + var deltaFilePath = LoaderTestResources.Delta.WriteToFile(directory.DirectoryPath, "Delta.dll"); + + var loader = new TestDefaultExtensionAssemblyLoader(Path.Combine(directory.DirectoryPath, "shadow")); + var checker = new DefaultExtensionDependencyChecker(loader, output, output); + + // This will cause the loader to cache some inconsistent information. + loader.LoadFromPath(alphaFilePath); + LoaderTestResources.Alpha.WriteToFile(directory.DirectoryPath, "Alpha.dll"); + + // Act + var result = checker.Check(new[] { alphaFilePath, gammaFilePath, deltaFilePath, }); + + // Assert + Assert.False(result, "Check should not have passed: " + output.ToString()); + } + } + + [Fact] + public void Check_ReturnsFalse_WhenLoaderThrows() + { + using (var directory = TempDirectory.Create()) + { + // Arrange + var output = new StringWriter(); + + var deltaFilePath = LoaderTestResources.Delta.WriteToFile(directory.DirectoryPath, "Delta.dll"); + + var loader = new Mock(); + loader + .Setup(l => l.LoadFromPath(It.IsAny())) + .Throws(new InvalidOperationException()); + var checker = new DefaultExtensionDependencyChecker(loader.Object, output, output); + + // Act + var result = checker.Check(new[] { deltaFilePath, }); + + // Assert + Assert.False(result, "Check should not have passed: " + output.ToString()); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Razor.Tools.Test/DefaultRequestDispatcherTest.cs b/test/Microsoft.AspNetCore.Razor.Tools.Test/DefaultRequestDispatcherTest.cs index 4e4f702fed..495649bd3e 100644 --- a/test/Microsoft.AspNetCore.Razor.Tools.Test/DefaultRequestDispatcherTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Tools.Test/DefaultRequestDispatcherTest.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Moq; @@ -335,7 +336,7 @@ namespace Microsoft.AspNetCore.Razor.Tools /// /// Ensure server respects keep alive and shuts down after processing simultaneous connections. /// - [Fact(Skip = "https://github.com/aspnet/Razor/issues/2018")] + [Fact] public async Task Dispatcher_ProcessSimultaneousConnections_HitsKeepAliveTimeout() { // Arrange @@ -352,6 +353,7 @@ namespace Microsoft.AspNetCore.Razor.Tools var source = new TaskCompletionSource(); var connectionTask = CreateConnectionWithEmptyServerRequest(c => { + // Keep the connection active until we decide to end it. c.WaitForDisconnectAsyncFunc = _ => source.Task; }); list.Add(source); @@ -370,24 +372,42 @@ namespace Microsoft.AspNetCore.Razor.Tools }; }); - var keepAlive = TimeSpan.FromSeconds(1); var eventBus = new TestableEventBus(); + var completedCompilations = 0; + var allCompilationsComplete = new TaskCompletionSource(); + eventBus.CompilationComplete += (obj, args) => + { + if (++completedCompilations == totalCount) + { + // All compilations have completed. + allCompilationsComplete.SetResult(true); + } + }; + var keepAlive = TimeSpan.FromSeconds(1); var dispatcherTask = Task.Run(() => { var dispatcher = new DefaultRequestDispatcher(connectionHost.Object, compilerHost, CancellationToken.None, eventBus, keepAlive); dispatcher.Run(); }); + // Wait for all connections to be created. await readySource.Task; + + // Wait for all compilations to complete. + await allCompilationsComplete.Task; + + // Now allow all the connections to be disconnected. foreach (var source in list) { source.SetResult(true); } // Act + // Now dispatcher should be in an idle state with no active connections. await dispatcherTask; // Assert + Assert.False(eventBus.HasDetectedBadConnection); Assert.Equal(totalCount, eventBus.CompletedCount); Assert.True(eventBus.LastProcessedTime.HasValue, "LastProcessedTime should have had a value."); Assert.True(eventBus.HitKeepAliveTimeout, "HitKeepAliveTimeout should have been hit."); diff --git a/test/Microsoft.AspNetCore.Razor.Tools.Test/Infrastructure/ServerUtilities.cs b/test/Microsoft.AspNetCore.Razor.Tools.Test/Infrastructure/ServerUtilities.cs index 473b608142..d6494d1d37 100644 --- a/test/Microsoft.AspNetCore.Razor.Tools.Test/Infrastructure/ServerUtilities.cs +++ b/test/Microsoft.AspNetCore.Razor.Tools.Test/Infrastructure/ServerUtilities.cs @@ -6,6 +6,8 @@ using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Moq; namespace Microsoft.AspNetCore.Razor.Tools { @@ -24,7 +26,8 @@ namespace Microsoft.AspNetCore.Razor.Tools internal static ServerData CreateServer( string pipeName = null, CompilerHost compilerHost = null, - ConnectionHost connectionHost = null) + ConnectionHost connectionHost = null, + Action onListening = null) { pipeName = pipeName ?? Guid.NewGuid().ToString(); compilerHost = compilerHost ?? CompilerHost.Create(); @@ -38,6 +41,10 @@ namespace Microsoft.AspNetCore.Razor.Tools { var eventBus = new TestableEventBus(); eventBus.Listening += (sender, e) => { serverListenSource.TrySetResult(true); }; + if (onListening != null) + { + eventBus.Listening += (sender, e) => onListening(sender, e); + } try { RunServer( @@ -116,7 +123,7 @@ namespace Microsoft.AspNetCore.Razor.Tools CancellationToken ct, EventBus eventBus, TimeSpan? keepAlive) - : base(new Application(ct)) + : base(new Application(ct, Mock.Of(), Mock.Of(), (path, properties) => Mock.Of())) { _host = host; _compilerHost = compilerHost; diff --git a/test/Microsoft.AspNetCore.Razor.Tools.Test/Infrastructure/TestableEventBus.cs b/test/Microsoft.AspNetCore.Razor.Tools.Test/Infrastructure/TestableEventBus.cs index 4bc2465476..6430ce05df 100644 --- a/test/Microsoft.AspNetCore.Razor.Tools.Test/Infrastructure/TestableEventBus.cs +++ b/test/Microsoft.AspNetCore.Razor.Tools.Test/Infrastructure/TestableEventBus.cs @@ -2,21 +2,27 @@ // 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.Text; namespace Microsoft.AspNetCore.Razor.Tools { internal class TestableEventBus : EventBus { - public int ListeningCount; - public int ConnectionCount; - public int CompletedCount; - public DateTime? LastProcessedTime; - public TimeSpan? KeepAlive; - public bool HasDetectedBadConnection; - public bool HitKeepAliveTimeout; public event EventHandler Listening; + public event EventHandler CompilationComplete; + + public int ListeningCount { get; private set; } + + public int ConnectionCount { get; private set; } + + public int CompletedCount { get; private set; } + + public DateTime? LastProcessedTime { get; private set; } + + public TimeSpan? KeepAlive { get; private set; } + + public bool HasDetectedBadConnection { get; private set; } + + public bool HitKeepAliveTimeout { get; private set; } public override void ConnectionListening() { @@ -35,6 +41,11 @@ namespace Microsoft.AspNetCore.Razor.Tools LastProcessedTime = DateTime.Now; } + public override void CompilationCompleted() + { + CompilationComplete?.Invoke(this, EventArgs.Empty); + } + public override void UpdateKeepAlive(TimeSpan timeSpan) { KeepAlive = timeSpan; diff --git a/test/Microsoft.AspNetCore.Razor.Tools.Test/LoaderTestResources.cs b/test/Microsoft.AspNetCore.Razor.Tools.Test/LoaderTestResources.cs new file mode 100644 index 0000000000..9478c46684 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Tools.Test/LoaderTestResources.cs @@ -0,0 +1,146 @@ +// 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 System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + internal static class LoaderTestResources + { + static LoaderTestResources() + { + Delta = CreateAssemblyBlob("Delta", Array.Empty(), @" +using System.Text; + +namespace Delta +{ + public class D + { + public void Write(StringBuilder sb, string s) + { + sb.AppendLine(""Delta: "" + s); + } + } +} +"); + + Gamma = CreateAssemblyBlob("Gamma", new[] { Delta, }, @" +using System.Text; +using Delta; + +namespace Gamma +{ + public class G + { + public void Write(StringBuilder sb, string s) + { + D d = new D(); + + d.Write(sb, ""Gamma: "" + s); + } + } +} +"); + + Alpha = CreateAssemblyBlob("Alpha", new[] { Gamma, }, @" +using System.Text; +using Gamma; + +namespace Alpha +{ + public class A + { + public void Write(StringBuilder sb, string s) + { + G g = new G(); + + g.Write(sb, ""Alpha: "" + s); + } + } +} +"); + + Beta = CreateAssemblyBlob("Beta", new[] { Gamma, }, @" +using System.Text; +using Gamma; + +namespace Beta +{ + public class B + { + public void Write(StringBuilder sb, string s) + { + G g = new G(); + + g.Write(sb, ""Beta: "" + s); + } + } +} +"); + } + + public static AssemblyBlob Alpha { get; } + + public static AssemblyBlob Beta { get; } + + public static AssemblyBlob Delta { get; } + + public static AssemblyBlob Gamma { get; } + + private static AssemblyBlob CreateAssemblyBlob(string assemblyName, AssemblyBlob[] references, string text) + { + var defaultReferences = new[] + { + MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + }; + + var compilation = CSharpCompilation.Create( + assemblyName, + new[] { CSharpSyntaxTree.ParseText(text) }, + references.Select(r => r.ToMetadataReference()).Concat(defaultReferences), + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + using (var assemblyStream = new MemoryStream()) + using (var symbolStream = new MemoryStream()) + { + var result = compilation.Emit(assemblyStream, symbolStream); + Assert.Empty(result.Diagnostics); + + return new AssemblyBlob(assemblyName, assemblyStream.GetBuffer(), symbolStream.GetBuffer()); + } + } + + public class AssemblyBlob + { + public AssemblyBlob(string assemblyName, byte[] assemblyBytes, byte[] symbolBytes) + { + AssemblyName = assemblyName; + AssemblyBytes = assemblyBytes; + SymbolBytes = symbolBytes; + } + + public string AssemblyName { get; } + + public byte[] AssemblyBytes { get; } + + public byte[] SymbolBytes { get; } + + public MetadataReference ToMetadataReference() + { + return MetadataReference.CreateFromImage(AssemblyBytes); + } + + internal string WriteToFile(string directoryPath, string fileName) + { + var filePath = Path.Combine(directoryPath, fileName); + File.WriteAllBytes(filePath, AssemblyBytes); + return filePath; + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Tools.Test/MetadataCacheTest.cs b/test/Microsoft.AspNetCore.Razor.Tools.Test/MetadataCacheTest.cs new file mode 100644 index 0000000000..fd4e54197c --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Tools.Test/MetadataCacheTest.cs @@ -0,0 +1,103 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + public class MetadataCacheTest + { + [Fact] + public void GetMetadata_AddsToCache() + { + using (var directory = TempDirectory.Create()) + { + // Arrange + var metadataCache = new MetadataCache(); + var assemblyFilePath = LoaderTestResources.Delta.WriteToFile(directory.DirectoryPath, "Delta.dll"); + + // Act + var result = metadataCache.GetMetadata(assemblyFilePath); + + // Assert + Assert.NotNull(result); + Assert.Equal(1, metadataCache.Cache.Count); + } + } + + [Fact] + public void GetMetadata_UsesCache() + { + using (var directory = TempDirectory.Create()) + { + // Arrange + var metadataCache = new MetadataCache(); + var assemblyFilePath = LoaderTestResources.Delta.WriteToFile(directory.DirectoryPath, "Delta.dll"); + + // Act 1 + var result = metadataCache.GetMetadata(assemblyFilePath); + + // Assert 1 + Assert.NotNull(result); + Assert.Equal(1, metadataCache.Cache.Count); + + // Act 2 + var cacheResult = metadataCache.GetMetadata(assemblyFilePath); + + // Assert 2 + Assert.Same(result, cacheResult); + Assert.Equal(1, metadataCache.Cache.Count); + } + } + + [Fact] + public void GetMetadata_MultipleFiles_ReturnsDifferentResultsAndAddsToCache() + { + using (var directory = TempDirectory.Create()) + { + // Arrange + var metadataCache = new MetadataCache(); + var assemblyFilePath1 = LoaderTestResources.Delta.WriteToFile(directory.DirectoryPath, "Delta.dll"); + var assemblyFilePath2 = LoaderTestResources.Gamma.WriteToFile(directory.DirectoryPath, "Gamma.dll"); + + // Act + var result1 = metadataCache.GetMetadata(assemblyFilePath1); + var result2 = metadataCache.GetMetadata(assemblyFilePath2); + + // Assert + Assert.NotSame(result1, result2); + Assert.Equal(2, metadataCache.Cache.Count); + } + } + + [Fact] + public void GetMetadata_ReplacesCache_IfFileTimestampChanged() + { + using (var directory = TempDirectory.Create()) + { + // Arrange + var metadataCache = new MetadataCache(); + var assemblyFilePath = LoaderTestResources.Delta.WriteToFile(directory.DirectoryPath, "Delta.dll"); + + // Act 1 + var result = metadataCache.GetMetadata(assemblyFilePath); + + // Assert 1 + Assert.NotNull(result); + var entry = Assert.Single(metadataCache.Cache.TestingEnumerable); + Assert.Same(result, entry.Value.Metadata); + + // Act 2 + // Update the timestamp of the file + File.SetLastWriteTimeUtc(assemblyFilePath, File.GetLastWriteTimeUtc(assemblyFilePath).AddSeconds(1)); + var cacheResult = metadataCache.GetMetadata(assemblyFilePath); + + // Assert 2 + Assert.NotSame(result, cacheResult); + entry = Assert.Single(metadataCache.Cache.TestingEnumerable); + Assert.Same(cacheResult, entry.Value.Metadata); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Tools.Test/Microsoft.AspNetCore.Razor.Tools.Test.csproj b/test/Microsoft.AspNetCore.Razor.Tools.Test/Microsoft.AspNetCore.Razor.Tools.Test.csproj index 3778c1c8ca..df0bf3822a 100644 --- a/test/Microsoft.AspNetCore.Razor.Tools.Test/Microsoft.AspNetCore.Razor.Tools.Test.csproj +++ b/test/Microsoft.AspNetCore.Razor.Tools.Test/Microsoft.AspNetCore.Razor.Tools.Test.csproj @@ -17,4 +17,10 @@ + + + System + + + diff --git a/test/Microsoft.AspNetCore.Razor.Tools.Test/Properties/AssemblyInfo.cs b/test/Microsoft.AspNetCore.Razor.Tools.Test/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..f0aa552b16 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Tools.Test/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// 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.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] diff --git a/test/Microsoft.AspNetCore.Razor.Tools.Test/ServerLifecycleTest.cs b/test/Microsoft.AspNetCore.Razor.Tools.Test/ServerLifecycleTest.cs index 27dd7e619f..f91fbc5fdd 100644 --- a/test/Microsoft.AspNetCore.Razor.Tools.Test/ServerLifecycleTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Tools.Test/ServerLifecycleTest.cs @@ -116,15 +116,19 @@ namespace Microsoft.AspNetCore.Razor.Tools /// A shutdown request should not abort an existing compilation. It should be allowed to run to /// completion. /// - [Fact(Skip = "Skipping temporarily on non-windows. https://github.com/aspnet/Razor/issues/1991")] + [Fact] public async Task ServerRunning_ShutdownRequest_DoesNotAbortCompilation() { // Arrange - var completionSource = new TaskCompletionSource(); + var startCompilationSource = new TaskCompletionSource(); + var finishCompilationSource = new TaskCompletionSource(); var host = CreateCompilerHost(c => c.ExecuteFunc = (req, ct) => { + // At this point, the connection has been accepted and the compilation has started. + startCompilationSource.SetResult(true); + // We want this to keep running even after the shutdown is seen. - completionSource.Task.Wait(); + finishCompilationSource.Task.Wait(); return EmptyServerResponse; }); @@ -132,13 +136,16 @@ namespace Microsoft.AspNetCore.Razor.Tools { var compileTask = ServerUtilities.Send(serverData.PipeName, EmptyServerRequest); + // Wait for the request to go through and trigger compilation. + await startCompilationSource.Task; + // Act // The compilation is now in progress, send the shutdown. await ServerUtilities.SendShutdown(serverData.PipeName); Assert.False(compileTask.IsCompleted); // Now let the task complete. - completionSource.SetResult(true); + finishCompilationSource.SetResult(true); // Assert var response = await compileTask; @@ -152,15 +159,19 @@ namespace Microsoft.AspNetCore.Razor.Tools /// /// Multiple clients should be able to send shutdown requests to the server. /// - [Fact(Skip = "Skipping temporarily on non-windows. https://github.com/aspnet/Razor/issues/1991")] + [Fact] public async Task ServerRunning_MultipleShutdownRequests_HandlesSuccessfully() { // Arrange - var completionSource = new TaskCompletionSource(); + var startCompilationSource = new TaskCompletionSource(); + var finishCompilationSource = new TaskCompletionSource(); var host = CreateCompilerHost(c => c.ExecuteFunc = (req, ct) => { + // At this point, the connection has been accepted and the compilation has started. + startCompilationSource.SetResult(true); + // We want this to keep running even after the shutdown is seen. - completionSource.Task.Wait(); + finishCompilationSource.Task.Wait(); return EmptyServerResponse; }); @@ -168,6 +179,9 @@ namespace Microsoft.AspNetCore.Razor.Tools { var compileTask = ServerUtilities.Send(serverData.PipeName, EmptyServerRequest); + // Wait for the request to go through and trigger compilation. + await startCompilationSource.Task; + // Act for (var i = 0; i < 10; i++) { @@ -178,7 +192,7 @@ namespace Microsoft.AspNetCore.Razor.Tools } // Now let the task complete. - completionSource.SetResult(true); + finishCompilationSource.SetResult(true); // Assert var response = await compileTask; @@ -189,7 +203,9 @@ namespace Microsoft.AspNetCore.Razor.Tools } } - [Fact(Skip = "Skipping temporarily on non-windows. https://github.com/aspnet/Razor/issues/1991")] + // https://github.com/aspnet/Razor/issues/1991 + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)] public async Task ServerRunning_CancelCompilation_CancelsSuccessfully() { // Arrange @@ -207,18 +223,28 @@ namespace Microsoft.AspNetCore.Razor.Tools return new RejectedServerResponse(); }); - using (var serverData = ServerUtilities.CreateServer(compilerHost: host)) + var semaphore = new SemaphoreSlim(1); + Action onListening = (s, e) => { - var tasks = new List>(); + semaphore.Release(); + }; + using (var serverData = ServerUtilities.CreateServer(compilerHost: host, onListening: onListening)) + { + // Send all the requests. + var clients = new List(); for (var i = 0; i < requestCount; i++) { - var task = ServerUtilities.Send(serverData.PipeName, EmptyServerRequest); - tasks.Add(task); + // Wait for the server to start listening. + await semaphore.WaitAsync(TimeSpan.FromMinutes(1)); + + var client = await Client.ConnectAsync(serverData.PipeName, timeout: null, cancellationToken: default); + await EmptyServerRequest.WriteAsync(client.Stream); + clients.Add(client); } // Act // Wait until all of the connections are being processed by the server. - completionSource.Task.Wait(); + await completionSource.Task; // Now cancel var stats = await serverData.CancelAndCompleteAsync(); @@ -227,10 +253,13 @@ namespace Microsoft.AspNetCore.Razor.Tools Assert.Equal(requestCount, stats.Connections); Assert.Equal(requestCount, count); - foreach (var task in tasks) + // Read the server response to each client. + foreach (var client in clients) { + var task = ServerResponse.ReadAsync(client.Stream); // We expect this to throw because the stream is already closed. - await Assert.ThrowsAsync(() => task); + await Assert.ThrowsAnyAsync(() => task); + client.Dispose(); } } } diff --git a/test/Microsoft.AspNetCore.Razor.Tools.Test/TempDirectory.cs b/test/Microsoft.AspNetCore.Razor.Tools.Test/TempDirectory.cs new file mode 100644 index 0000000000..d491248465 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Tools.Test/TempDirectory.cs @@ -0,0 +1,30 @@ +// 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.AspNetCore.Razor.Tools +{ + internal class TempDirectory : IDisposable + { + public static TempDirectory Create() + { + var directoryPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("n")); + Directory.CreateDirectory(directoryPath); + return new TempDirectory(directoryPath); + } + + private TempDirectory(string directoryPath) + { + DirectoryPath = directoryPath; + } + + public string DirectoryPath { get; } + + public void Dispose() + { + Directory.Delete(DirectoryPath, recursive: true); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Tools.Test/TestDefaultExtensionAssemblyLoader.cs b/test/Microsoft.AspNetCore.Razor.Tools.Test/TestDefaultExtensionAssemblyLoader.cs new file mode 100644 index 0000000000..d272e1c005 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Tools.Test/TestDefaultExtensionAssemblyLoader.cs @@ -0,0 +1,25 @@ +// 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 System.Reflection; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + internal class TestDefaultExtensionAssemblyLoader : DefaultExtensionAssemblyLoader + { + public TestDefaultExtensionAssemblyLoader(string baseDirectory) + : base(baseDirectory) + { + } + + protected override Assembly LoadFromPathUnsafeCore(string filePath) + { + // Force a load from streams so we don't lock the files on disk. This way we can test + // shadow copying without leaving a mess behind. + var bytes = File.ReadAllBytes(filePath); + var stream = new MemoryStream(bytes); + return LoadContext.LoadFromStream(stream); + } + } +} diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultImportDocumentManagerIntegrationTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultImportDocumentManagerIntegrationTest.cs index 18d890faac..5494ba8a4a 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultImportDocumentManagerIntegrationTest.cs +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultImportDocumentManagerIntegrationTest.cs @@ -21,7 +21,7 @@ namespace Microsoft.VisualStudio.Editor.Razor var testImportsPath = "C:\\path\\to\\project\\_ViewImports.cshtml"; var tracker = Mock.Of(t => t.FilePath == filePath && t.ProjectPath == projectPath); var anotherTracker = Mock.Of(t => t.FilePath == anotherFilePath && t.ProjectPath == projectPath); - var templateEngineFactoryService = GetTemplateEngineFactoryService(); + var templateEngineFactoryService = GetProjectEngineFactoryService(); var fileChangeTracker = new Mock(); fileChangeTracker.Setup(f => f.FilePath).Returns(testImportsPath); var fileChangeTrackerFactory = new Mock(); @@ -58,12 +58,12 @@ namespace Microsoft.VisualStudio.Editor.Razor Assert.True(called); } - private RazorTemplateEngineFactoryService GetTemplateEngineFactoryService() + private RazorProjectEngineFactoryService GetProjectEngineFactoryService() { var projectManager = new Mock(); projectManager.Setup(p => p.Projects).Returns(Array.Empty()); - var service = new DefaultTemplateEngineFactoryService(projectManager.Object); + var service = new DefaultProjectEngineFactoryService(projectManager.Object); return service; } } diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultImportDocumentManagerTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultImportDocumentManagerTest.cs index 856461052f..c32bbd0af3 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultImportDocumentManagerTest.cs +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultImportDocumentManagerTest.cs @@ -137,12 +137,12 @@ namespace Microsoft.VisualStudio.Editor.Razor manager.OnUnsubscribed(tracker); } - private RazorTemplateEngineFactoryService GetTemplateEngineFactoryService() + private RazorProjectEngineFactoryService GetTemplateEngineFactoryService() { var projectManager = new Mock(); projectManager.Setup(p => p.Projects).Returns(Array.Empty()); - var service = new DefaultTemplateEngineFactoryService(projectManager.Object); + var service = new DefaultProjectEngineFactoryService(projectManager.Object); return service; } } diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultTemplateEngineFactoryServiceTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultProjectEngineFactoryServiceTest.cs similarity index 89% rename from test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultTemplateEngineFactoryServiceTest.cs rename to test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultProjectEngineFactoryServiceTest.cs index 5e83b06042..a9ef8baba3 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultTemplateEngineFactoryServiceTest.cs +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultProjectEngineFactoryServiceTest.cs @@ -14,9 +14,9 @@ using MvcLatest = Microsoft.AspNetCore.Mvc.Razor.Extensions; namespace Microsoft.VisualStudio.Editor.Razor { - public class DefaultTemplateEngineFactoryServiceTest + public class DefaultProjectEngineFactoryServiceTest { - public DefaultTemplateEngineFactoryServiceTest() + public DefaultProjectEngineFactoryServiceTest() { Project project = null; @@ -35,7 +35,7 @@ namespace Microsoft.VisualStudio.Editor.Razor public Workspace Workspace { get; } [Fact] - public void Create_CreatesDesignTimeTemplateEngine_ForLatest() + public void Create_CreatesTemplateEngine_ForLatest() { // Arrange var projectManager = new TestProjectSnapshotManager(Workspace); @@ -49,13 +49,12 @@ namespace Microsoft.VisualStudio.Editor.Razor new ProjectExtensibilityAssembly(new AssemblyIdentity("Microsoft.AspNetCore.Razor", new Version("2.0.0.0")))), }); - var factoryService = new DefaultTemplateEngineFactoryService(projectManager); + var factoryService = new DefaultProjectEngineFactoryService(projectManager); // Act var engine = factoryService.Create("/TestPath/SomePath/", b => { b.Features.Add(new MyCoolNewFeature()); - Assert.True(b.DesignTime); }); // Assert @@ -65,7 +64,7 @@ namespace Microsoft.VisualStudio.Editor.Razor } [Fact] - public void Create_CreatesDesignTimeTemplateEngine_ForVersion1_1() + public void Create_CreatesTemplateEngine_ForVersion1_1() { // Arrange var projectManager = new TestProjectSnapshotManager(Workspace); @@ -79,13 +78,12 @@ namespace Microsoft.VisualStudio.Editor.Razor new ProjectExtensibilityAssembly(new AssemblyIdentity("Microsoft.AspNetCore.Razor", new Version("1.1.3.0")))), }); - var factoryService = new DefaultTemplateEngineFactoryService(projectManager); + var factoryService = new DefaultProjectEngineFactoryService(projectManager); // Act var engine = factoryService.Create("/TestPath/SomePath/", b => { b.Features.Add(new MyCoolNewFeature()); - Assert.True(b.DesignTime); }); // Assert @@ -109,7 +107,7 @@ namespace Microsoft.VisualStudio.Editor.Razor new ProjectExtensibilityAssembly(new AssemblyIdentity("Microsoft.AspNetCore.Razor", new Version("1.0.0.0")))), }); - var factoryService = new DefaultTemplateEngineFactoryService(projectManager); + var factoryService = new DefaultProjectEngineFactoryService(projectManager); // Act var engine = factoryService.Create("/TestPath/SomePath/", b => @@ -138,13 +136,12 @@ namespace Microsoft.VisualStudio.Editor.Razor new ProjectExtensibilityAssembly(new AssemblyIdentity("Microsoft.AspNetCore.Razor", new Version("3.0.0.0")))), }); - var factoryService = new DefaultTemplateEngineFactoryService(projectManager); + var factoryService = new DefaultProjectEngineFactoryService(projectManager); // Act var engine = factoryService.Create("/TestPath/SomePath/", b => { b.Features.Add(new MyCoolNewFeature()); - Assert.True(b.DesignTime); }); // Assert @@ -159,13 +156,12 @@ namespace Microsoft.VisualStudio.Editor.Razor // Arrange var projectManager = new TestProjectSnapshotManager(Workspace); - var factoryService = new DefaultTemplateEngineFactoryService(projectManager); + var factoryService = new DefaultProjectEngineFactoryService(projectManager); // Act var engine = factoryService.Create("/TestPath/DifferentPath/", b => { b.Features.Add(new MyCoolNewFeature()); - Assert.True(b.DesignTime); }); // Assert @@ -181,13 +177,12 @@ namespace Microsoft.VisualStudio.Editor.Razor var projectManager = new TestProjectSnapshotManager(Workspace); projectManager.ProjectAdded(Project); - var factoryService = new DefaultTemplateEngineFactoryService(projectManager); + var factoryService = new DefaultProjectEngineFactoryService(projectManager); // Act var engine = factoryService.Create("/TestPath/DifferentPath/", b => { b.Features.Add(new MyCoolNewFeature()); - Assert.True(b.DesignTime); }); // Assert diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioRazorParserIntegrationTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioRazorParserIntegrationTest.cs index 9b38140366..2102506035 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioRazorParserIntegrationTest.cs +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioRazorParserIntegrationTest.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; using System.Threading; @@ -525,34 +524,27 @@ namespace Microsoft.VisualStudio.Editor.Razor return new TestParserManager(parser); } - private static RazorTemplateEngineFactoryService CreateTemplateEngineFactory( + private static RazorProjectEngineFactoryService CreateTemplateEngineFactory( string path = TestLinePragmaFileName, IEnumerable tagHelpers = null) { - var engine = RazorEngine.CreateDesignTime(builder => + var fileSystem = new TestRazorProjectFileSystem(); + var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, fileSystem, builder => { RazorExtensions.Register(builder); + builder.AddDefaultImports("@addTagHelper *, Test"); + if (tagHelpers != null) { builder.AddTagHelpers(tagHelpers); } }); - // GetImports on RazorTemplateEngine will at least check that the item exists, so we need to pretend - // that it does. - var items = new List(); - items.Add(new TestRazorProjectItem(path)); + var projectEngineFactoryService = Mock.Of( + service => service.Create(It.IsAny(), It.IsAny>()) == projectEngine); - var project = new TestRazorProjectFileSystem(items); - - var templateEngine = new RazorTemplateEngine(engine, project); - templateEngine.Options.DefaultImports = RazorSourceDocument.Create("@addTagHelper *, Test", "_TestImports.cshtml"); - - var templateEngineFactory = Mock.Of( - service => service.Create(It.IsAny(), It.IsAny>()) == templateEngine); - - return templateEngineFactory; + return projectEngineFactoryService; } private async Task RunTypeKeywordTestAsync(string keyword) diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioRazorParserTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioRazorParserTest.cs index 38075b0460..8747324211 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioRazorParserTest.cs +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioRazorParserTest.cs @@ -32,7 +32,7 @@ namespace Microsoft.VisualStudio.Editor.Razor var parser = new DefaultVisualStudioRazorParser( Dispatcher, CreateDocumentTracker(), - Mock.Of(), + Mock.Of(), new DefaultErrorReporter(), Mock.Of()); parser.Dispose(); @@ -48,7 +48,7 @@ namespace Microsoft.VisualStudio.Editor.Razor var parser = new DefaultVisualStudioRazorParser( Dispatcher, CreateDocumentTracker(), - Mock.Of(), + Mock.Of(), new DefaultErrorReporter(), Mock.Of()); parser.Dispose(); @@ -64,7 +64,7 @@ namespace Microsoft.VisualStudio.Editor.Razor var parser = new DefaultVisualStudioRazorParser( Dispatcher, CreateDocumentTracker(), - Mock.Of(), + Mock.Of(), new DefaultErrorReporter(), Mock.Of()); parser.Dispose(); @@ -80,7 +80,7 @@ namespace Microsoft.VisualStudio.Editor.Razor using (var parser = new DefaultVisualStudioRazorParser( Dispatcher, CreateDocumentTracker(), - Mock.Of(), + Mock.Of(), new DefaultErrorReporter(), Mock.Of())) { @@ -108,7 +108,7 @@ namespace Microsoft.VisualStudio.Editor.Razor using (var parser = new DefaultVisualStudioRazorParser( Dispatcher, documentTracker, - Mock.Of(), + Mock.Of(), new DefaultErrorReporter(), Mock.Of())) { @@ -139,7 +139,7 @@ namespace Microsoft.VisualStudio.Editor.Razor using (var parser = new DefaultVisualStudioRazorParser( Dispatcher, CreateDocumentTracker(), - Mock.Of(), + Mock.Of(), new DefaultErrorReporter(), Mock.Of()) { @@ -169,7 +169,7 @@ namespace Microsoft.VisualStudio.Editor.Razor using (var parser = new DefaultVisualStudioRazorParser( Dispatcher, CreateDocumentTracker(), - Mock.Of(), + Mock.Of(), new DefaultErrorReporter(), Mock.Of()) { @@ -198,7 +198,7 @@ namespace Microsoft.VisualStudio.Editor.Razor using (var parser = new DefaultVisualStudioRazorParser( Dispatcher, CreateDocumentTracker(), - Mock.Of(), + Mock.Of(), new DefaultErrorReporter(), Mock.Of())) { @@ -222,7 +222,7 @@ namespace Microsoft.VisualStudio.Editor.Razor using (var parser = new DefaultVisualStudioRazorParser( Dispatcher, documentTracker, - Mock.Of(), + Mock.Of(), new DefaultErrorReporter(), Mock.Of())) { @@ -242,7 +242,7 @@ namespace Microsoft.VisualStudio.Editor.Razor using (var parser = new DefaultVisualStudioRazorParser( Dispatcher, CreateDocumentTracker(isSupportedProject: true), - Mock.Of(), + Mock.Of(), new DefaultErrorReporter(), Mock.Of())) { @@ -261,7 +261,7 @@ namespace Microsoft.VisualStudio.Editor.Razor using (var parser = new DefaultVisualStudioRazorParser( Dispatcher, CreateDocumentTracker(isSupportedProject: false), - Mock.Of(), + Mock.Of(), new DefaultErrorReporter(), Mock.Of())) { diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/RazorSyntaxTreePartialParserTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/RazorSyntaxTreePartialParserTest.cs index 074fa0e1be..8b5ee4ef5a 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/RazorSyntaxTreePartialParserTest.cs +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/RazorSyntaxTreePartialParserTest.cs @@ -3,14 +3,12 @@ using System; using System.Collections.Generic; -using System.Linq; using Microsoft.AspNetCore.Mvc.Razor.Extensions; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Legacy; using Microsoft.VisualStudio.Test; using Microsoft.VisualStudio.Text; using Xunit; -using Span = Microsoft.AspNetCore.Razor.Language.Legacy.Span; namespace Microsoft.VisualStudio.Editor.Razor { @@ -44,12 +42,13 @@ namespace Microsoft.VisualStudio.Editor.Razor { builder.Build() }; - var templateEngine = CreateTemplateEngine(tagHelpers: descriptors); - var document = TestRazorCodeDocument.Create( - TestRazorSourceDocument.Create(edit.OldSnapshot.GetText()), - new[] { templateEngine.Options.DefaultImports }); - templateEngine.Engine.Process(document); - var syntaxTree = document.GetSyntaxTree(); + var projectEngine = CreateProjectEngine(tagHelpers: descriptors); + var projectItem = new TestRazorProjectItem("Index.cshtml") + { + Content = edit.OldSnapshot.GetText() + }; + var codeDocument = projectEngine.Process(projectItem); + var syntaxTree = codeDocument.GetSyntaxTree(); var parser = new RazorSyntaxTreePartialParser(syntaxTree); // Act @@ -115,12 +114,13 @@ namespace Microsoft.VisualStudio.Editor.Razor attribute.SetPropertyName("StringAttribute"); }); var descriptors = new[] { builder.Build() }; - var templateEngine = CreateTemplateEngine(tagHelpers: descriptors); - var document = TestRazorCodeDocument.Create( - TestRazorSourceDocument.Create(edit.OldSnapshot.GetText()), - new[] { templateEngine.Options.DefaultImports }); - templateEngine.Engine.Process(document); - var syntaxTree = document.GetSyntaxTree(); + var projectEngine = CreateProjectEngine(tagHelpers: descriptors); + var sourceDocument = new TestRazorProjectItem("Index.cshtml") + { + Content = edit.OldSnapshot.GetText() + }; + var codeDocument = projectEngine.Process(sourceDocument); + var syntaxTree = codeDocument.GetSyntaxTree(); var parser = new RazorSyntaxTreePartialParser(syntaxTree); // Act @@ -548,7 +548,7 @@ namespace Microsoft.VisualStudio.Editor.Razor private void RunPartialParseRejectionTest(TestEdit edit, PartialParseResultInternal additionalFlags = 0) { - var templateEngine = CreateTemplateEngine(); + var templateEngine = CreateProjectEngine(); var document = TestRazorCodeDocument.Create(edit.OldSnapshot.GetText()); templateEngine.Engine.Process(document); var syntaxTree = document.GetSyntaxTree(); @@ -560,7 +560,7 @@ namespace Microsoft.VisualStudio.Editor.Razor private static void RunPartialParseTest(TestEdit edit, Block expectedTree, PartialParseResultInternal additionalFlags = 0) { - var templateEngine = CreateTemplateEngine(); + var templateEngine = CreateProjectEngine(); var document = TestRazorCodeDocument.Create(edit.OldSnapshot.GetText()); templateEngine.Engine.Process(document); var syntaxTree = document.GetSyntaxTree(); @@ -580,30 +580,24 @@ namespace Microsoft.VisualStudio.Editor.Razor return new TestEdit(sourceChange, oldSnapshot, changedSnapshot); } - private static RazorTemplateEngine CreateTemplateEngine( + private static RazorProjectEngine CreateProjectEngine( string path = "C:\\This\\Path\\Is\\Just\\For\\Line\\Pragmas.cshtml", IEnumerable tagHelpers = null) { - var engine = RazorEngine.CreateDesignTime(builder => + var fileSystem = new TestRazorProjectFileSystem(); + var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, fileSystem, builder => { RazorExtensions.Register(builder); + builder.AddDefaultImports("@addTagHelper *, Test"); + if (tagHelpers != null) { builder.AddTagHelpers(tagHelpers); } }); - // GetImports on RazorTemplateEngine will at least check that the item exists, so we need to pretend - // that it does. - var items = new List(); - items.Add(new TestRazorProjectItem(path)); - - var project = new TestRazorProjectFileSystem(items); - - var templateEngine = new RazorTemplateEngine(engine, project); - templateEngine.Options.DefaultImports = RazorSourceDocument.Create("@addTagHelper *, Test", "_TestImports.cshtml"); - return templateEngine; + return projectEngine; } } } diff --git a/test/RazorPageGenerator.Test/RazorPageGeneratorTest.cs b/test/RazorPageGenerator.Test/RazorPageGeneratorTest.cs index 81465d9541..613df16b0c 100644 --- a/test/RazorPageGenerator.Test/RazorPageGeneratorTest.cs +++ b/test/RazorPageGenerator.Test/RazorPageGeneratorTest.cs @@ -35,9 +35,10 @@ namespace RazorPageGenerator.Test { // Arrange var projectDirectory = TestProject.GetProjectDirectory(GetType()); - var razorEngine = Program.CreateRazorEngine("Microsoft.AspNetCore.TestGenerated"); + var projectEngine = Program.CreateProjectEngine("Microsoft.AspNetCore.TestGenerated", projectDirectory); + // Act - var results = Program.MainCore(razorEngine, projectDirectory); + var results = Program.MainCore(projectEngine, projectDirectory); // Assert Assert.Collection(results, diff --git a/test/testapps/AppWithP2PReference/AppWithP2PReference.csproj b/test/testapps/AppWithP2PReference/AppWithP2PReference.csproj index fe9d5d2940..b0ca8cfd76 100644 --- a/test/testapps/AppWithP2PReference/AppWithP2PReference.csproj +++ b/test/testapps/AppWithP2PReference/AppWithP2PReference.csproj @@ -1,13 +1,15 @@ - - - + <_RazorMSBuildRoot>$(SolutionRoot)src\Microsoft.AspNetCore.Razor.Design\bin\$(Configuration)\netstandard2.0\ - - + + + + <_MvcExtensionAssemblyPath>$(SolutionRoot)src\Microsoft.AspNetCore.Mvc.Razor.Extensions\bin\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.Mvc.Razor.Extensions.dll + + netcoreapp2.0 @@ -19,8 +21,5 @@ - - - - + diff --git a/test/testapps/ClassLibrary/ClassLibrary.csproj b/test/testapps/ClassLibrary/ClassLibrary.csproj index 443d790efa..1da425635e 100644 --- a/test/testapps/ClassLibrary/ClassLibrary.csproj +++ b/test/testapps/ClassLibrary/ClassLibrary.csproj @@ -1,33 +1,23 @@ - - - + <_RazorMSBuildRoot>$(SolutionRoot)src\Microsoft.AspNetCore.Razor.Design\bin\$(Configuration)\netstandard2.0\ - - - netcoreapp2.0 + + <_MvcExtensionAssemblyPath>$(SolutionRoot)src\Microsoft.AspNetCore.Mvc.Razor.Extensions\bin\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.Mvc.Razor.Extensions.dll + + - - RazorSDK + + netstandard2.0 - - - false - - - - - - - + diff --git a/test/testapps/Directory.Build.props b/test/testapps/Directory.Build.props index 475a18a63f..20497a3aa4 100644 --- a/test/testapps/Directory.Build.props +++ b/test/testapps/Directory.Build.props @@ -4,11 +4,15 @@ $(MSBuildThisFileDirectory)..\..\ $([MSBuild]::EnsureTrailingSlash('$(SolutionRoot)')) + + + $(SolutionRoot)src\Microsoft.NET.Sdk.Razor\build\netstandard2.0\Sdk.Razor.CurrentVersion.props + $(SolutionRoot)src\Microsoft.NET.Sdk.Razor\build\netstandard2.0\Sdk.Razor.CurrentVersion.targets - + @@ -17,7 +21,7 @@ - + diff --git a/test/testapps/RazorTest.Introspection.targets b/test/testapps/RazorTest.Introspection.targets index c950594f49..c78b200437 100644 --- a/test/testapps/RazorTest.Introspection.targets +++ b/test/testapps/RazorTest.Introspection.targets @@ -6,4 +6,8 @@ + + + + diff --git a/test/testapps/SimpleMvc/SimpleMvc.csproj b/test/testapps/SimpleMvc/SimpleMvc.csproj index 31c006b2f3..21ea1afc3c 100644 --- a/test/testapps/SimpleMvc/SimpleMvc.csproj +++ b/test/testapps/SimpleMvc/SimpleMvc.csproj @@ -1,14 +1,16 @@ - - - + <_RazorMSBuildRoot>$(SolutionRoot)src\Microsoft.AspNetCore.Razor.Design\bin\$(Configuration)\netstandard2.0\ - - + + + + <_MvcExtensionAssemblyPath>$(SolutionRoot)src\Microsoft.AspNetCore.Mvc.Razor.Extensions\bin\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.Mvc.Razor.Extensions.dll + + netcoreapp2.0 @@ -16,9 +18,6 @@ - - - - + diff --git a/test/testapps/SimpleMvc/SimpleTagHelper.cs b/test/testapps/SimpleMvc/SimpleTagHelper.cs new file mode 100644 index 0000000000..7cc52922d9 --- /dev/null +++ b/test/testapps/SimpleMvc/SimpleTagHelper.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Razor.TagHelpers; + +namespace SimpleMvc +{ + public class SimpleTagHelper : TagHelper + { + } +} diff --git a/test/testapps/SimplePages/SimplePages.csproj b/test/testapps/SimplePages/SimplePages.csproj index 340a6f14db..2928d33d45 100644 --- a/test/testapps/SimplePages/SimplePages.csproj +++ b/test/testapps/SimplePages/SimplePages.csproj @@ -1,14 +1,16 @@ - + - - <_RazorMSBuildRoot>$(SolutionRoot)src\Microsoft.AspNetCore.Razor.Design\bin\$(Configuration)\netstandard2.0\ - - + + + + <_MvcExtensionAssemblyPath>$(SolutionRoot)src\Microsoft.AspNetCore.Mvc.Razor.Extensions\bin\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.Mvc.Razor.Extensions.dll + + netcoreapp2.0 @@ -16,9 +18,6 @@ - - - - + diff --git a/tooling/Microsoft.VisualStudio.RazorExtension/Microsoft.VisualStudio.RazorExtension.csproj b/tooling/Microsoft.VisualStudio.RazorExtension/Microsoft.VisualStudio.RazorExtension.csproj index 85c7323bf5..67dc3bf0e1 100644 --- a/tooling/Microsoft.VisualStudio.RazorExtension/Microsoft.VisualStudio.RazorExtension.csproj +++ b/tooling/Microsoft.VisualStudio.RazorExtension/Microsoft.VisualStudio.RazorExtension.csproj @@ -268,6 +268,21 @@ + + + + true + MSBuild + Microsoft\VisualStudio\Razor\ + + + + true + MSBuild + Microsoft\VisualStudio\Razor\Rules\ + + + + diff --git a/version.props b/version.props index 64f2d97dbc..5965a8e4d2 100644 --- a/version.props +++ b/version.props @@ -5,6 +5,8 @@ $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix)-final t000 + a- + $(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-')) $(VersionSuffix)-$(BuildNumber) 99999