From f23ff9452c5c738eba2ec5fcf3ee8f17f8861cfa Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Mon, 18 Sep 2017 19:11:39 -0700 Subject: [PATCH] [Design] Create Template engine from project snapshot --- .../DefaultTemplateEngineFactoryService.cs | 61 ++++---- ...aultTemplateEngineFactoryServiceFactory.cs | 3 +- .../LegacyTemplateEngineFactoryService.cs | 48 +++++-- ...DefaultTemplateEngineFactoryServiceTest.cs | 131 +++++++++++------- 4 files changed, 147 insertions(+), 96 deletions(-) diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTemplateEngineFactoryService.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTemplateEngineFactoryService.cs index 321dfcdfe7..e24938dccd 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTemplateEngineFactoryService.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTemplateEngineFactoryService.cs @@ -3,10 +3,8 @@ using System; using System.IO; -using System.Linq; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Mvc1_X = Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X; using MvcLatest = Microsoft.AspNetCore.Mvc.Razor.Extensions; @@ -14,14 +12,21 @@ namespace Microsoft.CodeAnalysis.Razor { internal class DefaultTemplateEngineFactoryService : RazorTemplateEngineFactoryService { - private const string MvcAssemblyName = "Microsoft.AspNetCore.Mvc.Razor"; - private static readonly Version LatestSupportedMvc = new Version(2, 1, 0); + private readonly static MvcExtensibilityConfiguration DefaultConfiguration = new MvcExtensibilityConfiguration( + ProjectExtensibilityConfigurationKind.Fallback, + new ProjectExtensibilityAssembly(new AssemblyIdentity("Microsoft.AspNetCore.Razor.Language", new Version("2.0.0.0"))), + new ProjectExtensibilityAssembly(new AssemblyIdentity("Microsoft.AspNetCore.Mvc.Razor", new Version("2.0.0.0")))); - private readonly HostLanguageServices _services; + private readonly ProjectSnapshotManager _projectManager; - public DefaultTemplateEngineFactoryService(HostLanguageServices services) + public DefaultTemplateEngineFactoryService(ProjectSnapshotManager projectManager) { - _services = services; + if (projectManager == null) + { + throw new ArgumentNullException(nameof(projectManager)); + } + + _projectManager = projectManager; } public override RazorTemplateEngine Create(string projectPath, Action configure) @@ -31,9 +36,12 @@ namespace Microsoft.CodeAnalysis.Razor throw new ArgumentNullException(nameof(projectPath)); } + // In 15.5 we expect projectPath to be a directory, NOT the path to the csproj. + var project = FindProject(projectPath); + var configuration = (project?.Configuration as MvcExtensibilityConfiguration) ?? DefaultConfiguration; + RazorEngine engine; - var mvcVersion = GetMvcVersion(projectPath); - if (mvcVersion?.Major == 1) + if (configuration.RazorAssembly.Identity.Version.Major == 1) { engine = RazorEngine.CreateDesignTime(b => { @@ -41,7 +49,7 @@ namespace Microsoft.CodeAnalysis.Razor Mvc1_X.RazorExtensions.Register(b); - if (mvcVersion?.Minor >= 1) + if (configuration.MvcAssembly.Identity.Version.Minor >= 1) { Mvc1_X.RazorExtensions.RegisterViewComponentTagHelpers(b); } @@ -53,12 +61,6 @@ namespace Microsoft.CodeAnalysis.Razor } else { - if (mvcVersion?.Major != LatestSupportedMvc.Major) - { - // TODO: Log unknown Mvc version. Something like - // Could not construct Razor engine for Mvc version '{mvcVersion}'. Falling back to Razor engine for Mvc '{LatestSupportedMvc}'. - } - engine = RazorEngine.CreateDesignTime(b => { configure?.Invoke(b); @@ -72,28 +74,19 @@ namespace Microsoft.CodeAnalysis.Razor } } - private Version GetMvcVersion(string projectPath) + private ProjectSnapshot FindProject(string directory) { - var workspace = _services.WorkspaceServices.Workspace; + directory = NormalizeDirectoryPath(directory); - var project = workspace.CurrentSolution.Projects.FirstOrDefault(p => + var projects = _projectManager.Projects; + for (var i = 0; i < projects.Count; i++) { - var directory = Path.GetDirectoryName(p.FilePath); - return string.Equals( - NormalizeDirectoryPath(directory), - NormalizeDirectoryPath(projectPath), - StringComparison.OrdinalIgnoreCase); - }); - - if (project != null) - { - var compilation = CSharpCompilation.Create(project.AssemblyName).AddReferences(project.MetadataReferences); - - foreach (var identity in compilation.ReferencedAssemblyNames) + var project = projects[i]; + if (project.UnderlyingProject.FilePath != null) { - if (identity.Name == MvcAssemblyName) + if (string.Equals(directory, NormalizeDirectoryPath(Path.GetDirectoryName(project.UnderlyingProject.FilePath)), StringComparison.OrdinalIgnoreCase)) { - return identity.Version; + return project; } } } diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTemplateEngineFactoryServiceFactory.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTemplateEngineFactoryServiceFactory.cs index c08c665cd5..66fe790807 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTemplateEngineFactoryServiceFactory.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTemplateEngineFactoryServiceFactory.cs @@ -4,6 +4,7 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; namespace Microsoft.VisualStudio.LanguageServices.Razor { @@ -12,7 +13,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor { public ILanguageService CreateLanguageService(HostLanguageServices languageServices) { - return new DefaultTemplateEngineFactoryService(languageServices); + return new DefaultTemplateEngineFactoryService(languageServices.GetRequiredService()); } } } \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/LegacyTemplateEngineFactoryService.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/LegacyTemplateEngineFactoryService.cs index 4747493a2f..fcae83b4ec 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/LegacyTemplateEngineFactoryService.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/LegacyTemplateEngineFactoryService.cs @@ -3,8 +3,10 @@ using System; using System.ComponentModel.Composition; -using Microsoft.AspNetCore.Mvc.Razor.Extensions; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor; +using Inner = Microsoft.CodeAnalysis.Razor.RazorTemplateEngineFactoryService; namespace Microsoft.VisualStudio.LanguageServices.Razor { @@ -15,6 +17,38 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor [Export(typeof(RazorTemplateEngineFactoryService))] internal class LegacyTemplateEngineFactoryService : RazorTemplateEngineFactoryService { + private readonly Inner _inner; + private readonly Workspace _workspace; + + [ImportingConstructor] + public LegacyTemplateEngineFactoryService([Import(typeof(VisualStudioWorkspace))] Workspace workspace) + { + if (workspace == null) + { + throw new ArgumentNullException(nameof(workspace)); + } + + _workspace = workspace; + _inner = workspace.Services.GetLanguageServices(RazorLanguage.Name).GetRequiredService(); + } + + // internal for testing + internal LegacyTemplateEngineFactoryService(Workspace workspace, Inner inner) + { + if (workspace == null) + { + throw new ArgumentNullException(nameof(workspace)); + } + + if (inner == null) + { + throw new ArgumentNullException(nameof(inner)); + } + + _workspace = workspace; + _inner = inner; + } + public override RazorTemplateEngine Create(string projectPath, Action configure) { if (projectPath == null) @@ -22,17 +56,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor throw new ArgumentNullException(nameof(projectPath)); } - var engine = RazorEngine.CreateDesignTime(b => - { - configure?.Invoke(b); - - // For now we're hardcoded to use MVC's extensibility. - RazorExtensions.Register(b); - }); - - var templateEngine = new MvcRazorTemplateEngine(engine, RazorProject.Create(projectPath)); - templateEngine.Options.ImportsFileName = "_ViewImports.cshtml"; - return templateEngine; + return _inner.Create(projectPath, configure); } } } diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DefaultTemplateEngineFactoryServiceTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DefaultTemplateEngineFactoryServiceTest.cs index 30af758179..3930d395ef 100644 --- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DefaultTemplateEngineFactoryServiceTest.cs +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DefaultTemplateEngineFactoryServiceTest.cs @@ -8,18 +8,43 @@ using Microsoft.CodeAnalysis.Host; using Mvc1_X = Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X; using MvcLatest = Microsoft.AspNetCore.Mvc.Razor.Extensions; using Xunit; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using System.Collections.Generic; +using Moq; +using System; namespace Microsoft.CodeAnalysis.Razor { public class DefaultTemplateEngineFactoryServiceTest { + public DefaultTemplateEngineFactoryServiceTest() + { + Workspace = new AdhocWorkspace(); + + var info = ProjectInfo.Create(ProjectId.CreateNewId("Test"), VersionStamp.Default, "Test", "Test", LanguageNames.CSharp, filePath: "/TestPath/SomePath/Test.csproj"); + Project = Workspace.CurrentSolution.AddProject(info).GetProject(info.Id); + } + + // We don't actually look at the project, we rely on the ProjectStateManager + public Project Project { get; } + + public Workspace Workspace { get; } + [Fact] public void Create_CreatesDesignTimeTemplateEngine_ForLatest() { // Arrange - var mvcReference = GetAssemblyMetadataReference("Microsoft.AspNetCore.Mvc.Razor", "2.0.0"); - var services = GetServices(mvcReference); - var factoryService = new DefaultTemplateEngineFactoryService(services); + var projectManager = new TestProjectSnapshotManager(Workspace); + projectManager.ProjectAdded(Project); + projectManager.ProjectUpdated(new ProjectSnapshotUpdateContext(Project) + { + Configuration = new MvcExtensibilityConfiguration( + ProjectExtensibilityConfigurationKind.ApproximateMatch, + new ProjectExtensibilityAssembly(new AssemblyIdentity("Microsoft.AspNetCore.Mvc.Razor", new Version("2.0.0.0"))), + new ProjectExtensibilityAssembly(new AssemblyIdentity("Microsoft.AspNetCore.Razor", new Version("2.0.0.0")))), + }); + + var factoryService = new DefaultTemplateEngineFactoryService(projectManager); // Act var engine = factoryService.Create("/TestPath/SomePath/", b => @@ -38,9 +63,17 @@ namespace Microsoft.CodeAnalysis.Razor public void Create_CreatesDesignTimeTemplateEngine_ForVersion1_1() { // Arrange - var mvcReference = GetAssemblyMetadataReference("Microsoft.AspNetCore.Mvc.Razor", "1.1.3"); - var services = GetServices(mvcReference); - var factoryService = new DefaultTemplateEngineFactoryService(services); + var projectManager = new TestProjectSnapshotManager(Workspace); + projectManager.ProjectAdded(Project); + projectManager.ProjectUpdated(new ProjectSnapshotUpdateContext(Project) + { + Configuration = new MvcExtensibilityConfiguration( + ProjectExtensibilityConfigurationKind.ApproximateMatch, + new ProjectExtensibilityAssembly(new AssemblyIdentity("Microsoft.AspNetCore.Mvc.Razor", new Version("1.1.3.0"))), + new ProjectExtensibilityAssembly(new AssemblyIdentity("Microsoft.AspNetCore.Razor", new Version("1.1.3.0")))), + }); + + var factoryService = new DefaultTemplateEngineFactoryService(projectManager); // Act var engine = factoryService.Create("/TestPath/SomePath/", b => @@ -59,9 +92,17 @@ namespace Microsoft.CodeAnalysis.Razor public void Create_DoesNotSupportViewComponentTagHelpers_ForVersion1_0() { // Arrange - var mvcReference = GetAssemblyMetadataReference("Microsoft.AspNetCore.Mvc.Razor", "1.0.0"); - var services = GetServices(mvcReference); - var factoryService = new DefaultTemplateEngineFactoryService(services); + var projectManager = new TestProjectSnapshotManager(Workspace); + projectManager.ProjectAdded(Project); + projectManager.ProjectUpdated(new ProjectSnapshotUpdateContext(Project) + { + Configuration = new MvcExtensibilityConfiguration( + ProjectExtensibilityConfigurationKind.ApproximateMatch, + new ProjectExtensibilityAssembly(new AssemblyIdentity("Microsoft.AspNetCore.Mvc.Razor", new Version("1.0.0.0"))), + new ProjectExtensibilityAssembly(new AssemblyIdentity("Microsoft.AspNetCore.Razor", new Version("1.0.0.0")))), + }); + + var factoryService = new DefaultTemplateEngineFactoryService(projectManager); // Act var engine = factoryService.Create("/TestPath/SomePath/", b => @@ -76,12 +117,20 @@ namespace Microsoft.CodeAnalysis.Razor } [Fact] - public void Create_UnknownMvcVersion_UsesLatest() + public void Create_HigherMvcVersion_UsesLatest() { // Arrange - var mvcReference = GetAssemblyMetadataReference("Microsoft.AspNetCore.Mvc.Razor", "3.0.0"); - var services = GetServices(mvcReference); - var factoryService = new DefaultTemplateEngineFactoryService(services); + var projectManager = new TestProjectSnapshotManager(Workspace); + projectManager.ProjectAdded(Project); + projectManager.ProjectUpdated(new ProjectSnapshotUpdateContext(Project) + { + Configuration = new MvcExtensibilityConfiguration( + ProjectExtensibilityConfigurationKind.ApproximateMatch, + new ProjectExtensibilityAssembly(new AssemblyIdentity("Microsoft.AspNetCore.Mvc.Razor", new Version("3.0.0.0"))), + new ProjectExtensibilityAssembly(new AssemblyIdentity("Microsoft.AspNetCore.Razor", new Version("3.0.0.0")))), + }); + + var factoryService = new DefaultTemplateEngineFactoryService(projectManager); // Act var engine = factoryService.Create("/TestPath/SomePath/", b => @@ -100,9 +149,9 @@ namespace Microsoft.CodeAnalysis.Razor public void Create_UnknownProjectPath_UsesLatest() { // Arrange - var mvcReference = GetAssemblyMetadataReference("Microsoft.AspNetCore.Mvc.Razor", "1.1.0"); - var services = GetServices(mvcReference); - var factoryService = new DefaultTemplateEngineFactoryService(services); + var projectManager = new TestProjectSnapshotManager(Workspace); + + var factoryService = new DefaultTemplateEngineFactoryService(projectManager); // Act var engine = factoryService.Create("/TestPath/DifferentPath/", b => @@ -121,9 +170,10 @@ namespace Microsoft.CodeAnalysis.Razor public void Create_MvcReferenceNotFound_UsesLatest() { // Arrange - var mvcReference = GetAssemblyMetadataReference("Microsoft.Something.Else", "1.0.0"); - var services = GetServices(mvcReference); - var factoryService = new DefaultTemplateEngineFactoryService(services); + var projectManager = new TestProjectSnapshotManager(Workspace); + projectManager.ProjectAdded(Project); + + var factoryService = new DefaultTemplateEngineFactoryService(projectManager); // Act var engine = factoryService.Create("/TestPath/DifferentPath/", b => @@ -138,39 +188,22 @@ namespace Microsoft.CodeAnalysis.Razor Assert.Single(engine.Engine.Features.OfType()); } - private HostLanguageServices GetServices(MetadataReference mvcReference) - { - var project = ProjectInfo - .Create(ProjectId.CreateNewId(), VersionStamp.Default, "TestProject", "TestAssembly", LanguageNames.CSharp) - .WithFilePath("/TestPath/SomePath/MyProject.csproj") - .WithMetadataReferences(new[] { mvcReference }); - - var workspace = new AdhocWorkspace(); - workspace.AddProject(project); - - return workspace.Services.GetLanguageServices(LanguageNames.CSharp); - } - - private MetadataReference GetAssemblyMetadataReference(string assemblyName, string version) - { - var code = $@" -using System.Reflection; -[assembly: AssemblyVersion(""{version}"")] -"; - - var syntaxTree = CSharpSyntaxTree.ParseText(code); - - var compilation = CSharpCompilation.Create( - assemblyName, - syntaxTrees: new[] { syntaxTree }, - references: new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) }); - - return compilation.ToMetadataReference(); - } - private class MyCoolNewFeature : IRazorEngineFeature { public RazorEngine Engine { get; set; } } + + private class TestProjectSnapshotManager : DefaultProjectSnapshotManager + { + public TestProjectSnapshotManager(Workspace workspace) + : base( + Mock.Of(), + Mock.Of(), + Mock.Of(), + Enumerable.Empty(), + workspace) + { + } + } } }