[Design] Create Template engine from project snapshot

This commit is contained in:
Ryan Nowak 2017-09-18 19:11:39 -07:00
parent 12e61d75a7
commit f23ff9452c
4 changed files with 147 additions and 96 deletions

View File

@ -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<IRazorEngineBuilder> 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;
}
}
}

View File

@ -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<ProjectSnapshotManager>());
}
}
}

View File

@ -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<Inner>();
}
// 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<IRazorEngineBuilder> 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);
}
}
}

View File

@ -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<MvcLatest.ViewComponentTagHelperPass>());
}
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<ForegroundDispatcher>(),
Mock.Of<ErrorReporter>(),
Mock.Of<ProjectSnapshotWorker>(),
Enumerable.Empty<ProjectSnapshotChangeTrigger>(),
workspace)
{
}
}
}
}