[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;
using System.IO; using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Host;
using Mvc1_X = Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X; using Mvc1_X = Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X;
using MvcLatest = Microsoft.AspNetCore.Mvc.Razor.Extensions; using MvcLatest = Microsoft.AspNetCore.Mvc.Razor.Extensions;
@ -14,14 +12,21 @@ namespace Microsoft.CodeAnalysis.Razor
{ {
internal class DefaultTemplateEngineFactoryService : RazorTemplateEngineFactoryService internal class DefaultTemplateEngineFactoryService : RazorTemplateEngineFactoryService
{ {
private const string MvcAssemblyName = "Microsoft.AspNetCore.Mvc.Razor"; private readonly static MvcExtensibilityConfiguration DefaultConfiguration = new MvcExtensibilityConfiguration(
private static readonly Version LatestSupportedMvc = new Version(2, 1, 0); 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) public override RazorTemplateEngine Create(string projectPath, Action<IRazorEngineBuilder> configure)
@ -31,9 +36,12 @@ namespace Microsoft.CodeAnalysis.Razor
throw new ArgumentNullException(nameof(projectPath)); 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; RazorEngine engine;
var mvcVersion = GetMvcVersion(projectPath); if (configuration.RazorAssembly.Identity.Version.Major == 1)
if (mvcVersion?.Major == 1)
{ {
engine = RazorEngine.CreateDesignTime(b => engine = RazorEngine.CreateDesignTime(b =>
{ {
@ -41,7 +49,7 @@ namespace Microsoft.CodeAnalysis.Razor
Mvc1_X.RazorExtensions.Register(b); Mvc1_X.RazorExtensions.Register(b);
if (mvcVersion?.Minor >= 1) if (configuration.MvcAssembly.Identity.Version.Minor >= 1)
{ {
Mvc1_X.RazorExtensions.RegisterViewComponentTagHelpers(b); Mvc1_X.RazorExtensions.RegisterViewComponentTagHelpers(b);
} }
@ -53,12 +61,6 @@ namespace Microsoft.CodeAnalysis.Razor
} }
else 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 => engine = RazorEngine.CreateDesignTime(b =>
{ {
configure?.Invoke(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); var project = projects[i];
return string.Equals( if (project.UnderlyingProject.FilePath != null)
NormalizeDirectoryPath(directory),
NormalizeDirectoryPath(projectPath),
StringComparison.OrdinalIgnoreCase);
});
if (project != null)
{
var compilation = CSharpCompilation.Create(project.AssemblyName).AddReferences(project.MetadataReferences);
foreach (var identity in compilation.ReferencedAssemblyNames)
{ {
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;
using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
namespace Microsoft.VisualStudio.LanguageServices.Razor namespace Microsoft.VisualStudio.LanguageServices.Razor
{ {
@ -12,7 +13,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
{ {
public ILanguageService CreateLanguageService(HostLanguageServices languageServices) 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;
using System.ComponentModel.Composition; using System.ComponentModel.Composition;
using Microsoft.AspNetCore.Mvc.Razor.Extensions;
using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Inner = Microsoft.CodeAnalysis.Razor.RazorTemplateEngineFactoryService;
namespace Microsoft.VisualStudio.LanguageServices.Razor namespace Microsoft.VisualStudio.LanguageServices.Razor
{ {
@ -15,6 +17,38 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
[Export(typeof(RazorTemplateEngineFactoryService))] [Export(typeof(RazorTemplateEngineFactoryService))]
internal class LegacyTemplateEngineFactoryService : 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) public override RazorTemplateEngine Create(string projectPath, Action<IRazorEngineBuilder> configure)
{ {
if (projectPath == null) if (projectPath == null)
@ -22,17 +56,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
throw new ArgumentNullException(nameof(projectPath)); throw new ArgumentNullException(nameof(projectPath));
} }
var engine = RazorEngine.CreateDesignTime(b => return _inner.Create(projectPath, configure);
{
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;
} }
} }
} }

View File

@ -8,18 +8,43 @@ using Microsoft.CodeAnalysis.Host;
using Mvc1_X = Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X; using Mvc1_X = Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X;
using MvcLatest = Microsoft.AspNetCore.Mvc.Razor.Extensions; using MvcLatest = Microsoft.AspNetCore.Mvc.Razor.Extensions;
using Xunit; using Xunit;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using System.Collections.Generic;
using Moq;
using System;
namespace Microsoft.CodeAnalysis.Razor namespace Microsoft.CodeAnalysis.Razor
{ {
public class DefaultTemplateEngineFactoryServiceTest 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] [Fact]
public void Create_CreatesDesignTimeTemplateEngine_ForLatest() public void Create_CreatesDesignTimeTemplateEngine_ForLatest()
{ {
// Arrange // Arrange
var mvcReference = GetAssemblyMetadataReference("Microsoft.AspNetCore.Mvc.Razor", "2.0.0"); var projectManager = new TestProjectSnapshotManager(Workspace);
var services = GetServices(mvcReference); projectManager.ProjectAdded(Project);
var factoryService = new DefaultTemplateEngineFactoryService(services); 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 // Act
var engine = factoryService.Create("/TestPath/SomePath/", b => var engine = factoryService.Create("/TestPath/SomePath/", b =>
@ -38,9 +63,17 @@ namespace Microsoft.CodeAnalysis.Razor
public void Create_CreatesDesignTimeTemplateEngine_ForVersion1_1() public void Create_CreatesDesignTimeTemplateEngine_ForVersion1_1()
{ {
// Arrange // Arrange
var mvcReference = GetAssemblyMetadataReference("Microsoft.AspNetCore.Mvc.Razor", "1.1.3"); var projectManager = new TestProjectSnapshotManager(Workspace);
var services = GetServices(mvcReference); projectManager.ProjectAdded(Project);
var factoryService = new DefaultTemplateEngineFactoryService(services); 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 // Act
var engine = factoryService.Create("/TestPath/SomePath/", b => var engine = factoryService.Create("/TestPath/SomePath/", b =>
@ -59,9 +92,17 @@ namespace Microsoft.CodeAnalysis.Razor
public void Create_DoesNotSupportViewComponentTagHelpers_ForVersion1_0() public void Create_DoesNotSupportViewComponentTagHelpers_ForVersion1_0()
{ {
// Arrange // Arrange
var mvcReference = GetAssemblyMetadataReference("Microsoft.AspNetCore.Mvc.Razor", "1.0.0"); var projectManager = new TestProjectSnapshotManager(Workspace);
var services = GetServices(mvcReference); projectManager.ProjectAdded(Project);
var factoryService = new DefaultTemplateEngineFactoryService(services); 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 // Act
var engine = factoryService.Create("/TestPath/SomePath/", b => var engine = factoryService.Create("/TestPath/SomePath/", b =>
@ -76,12 +117,20 @@ namespace Microsoft.CodeAnalysis.Razor
} }
[Fact] [Fact]
public void Create_UnknownMvcVersion_UsesLatest() public void Create_HigherMvcVersion_UsesLatest()
{ {
// Arrange // Arrange
var mvcReference = GetAssemblyMetadataReference("Microsoft.AspNetCore.Mvc.Razor", "3.0.0"); var projectManager = new TestProjectSnapshotManager(Workspace);
var services = GetServices(mvcReference); projectManager.ProjectAdded(Project);
var factoryService = new DefaultTemplateEngineFactoryService(services); 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 // Act
var engine = factoryService.Create("/TestPath/SomePath/", b => var engine = factoryService.Create("/TestPath/SomePath/", b =>
@ -100,9 +149,9 @@ namespace Microsoft.CodeAnalysis.Razor
public void Create_UnknownProjectPath_UsesLatest() public void Create_UnknownProjectPath_UsesLatest()
{ {
// Arrange // Arrange
var mvcReference = GetAssemblyMetadataReference("Microsoft.AspNetCore.Mvc.Razor", "1.1.0"); var projectManager = new TestProjectSnapshotManager(Workspace);
var services = GetServices(mvcReference);
var factoryService = new DefaultTemplateEngineFactoryService(services); var factoryService = new DefaultTemplateEngineFactoryService(projectManager);
// Act // Act
var engine = factoryService.Create("/TestPath/DifferentPath/", b => var engine = factoryService.Create("/TestPath/DifferentPath/", b =>
@ -121,9 +170,10 @@ namespace Microsoft.CodeAnalysis.Razor
public void Create_MvcReferenceNotFound_UsesLatest() public void Create_MvcReferenceNotFound_UsesLatest()
{ {
// Arrange // Arrange
var mvcReference = GetAssemblyMetadataReference("Microsoft.Something.Else", "1.0.0"); var projectManager = new TestProjectSnapshotManager(Workspace);
var services = GetServices(mvcReference); projectManager.ProjectAdded(Project);
var factoryService = new DefaultTemplateEngineFactoryService(services);
var factoryService = new DefaultTemplateEngineFactoryService(projectManager);
// Act // Act
var engine = factoryService.Create("/TestPath/DifferentPath/", b => var engine = factoryService.Create("/TestPath/DifferentPath/", b =>
@ -138,39 +188,22 @@ namespace Microsoft.CodeAnalysis.Razor
Assert.Single(engine.Engine.Features.OfType<MvcLatest.ViewComponentTagHelperPass>()); 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 private class MyCoolNewFeature : IRazorEngineFeature
{ {
public RazorEngine Engine { get; set; } 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)
{
}
}
} }
} }