diff --git a/benchmarks/Microsoft.AspNetCore.Razor.Performance/Microsoft.AspNetCore.Razor.Performance.csproj b/benchmarks/Microsoft.AspNetCore.Razor.Performance/Microsoft.AspNetCore.Razor.Performance.csproj index e63049a74f..790364d92e 100644 --- a/benchmarks/Microsoft.AspNetCore.Razor.Performance/Microsoft.AspNetCore.Razor.Performance.csproj +++ b/benchmarks/Microsoft.AspNetCore.Razor.Performance/Microsoft.AspNetCore.Razor.Performance.csproj @@ -10,14 +10,12 @@ + - - Shared\RazorDiagnosticJsonConverter.cs - - - Shared\TagHelperDescriptorJsonConverter.cs + + Serialization\%(FileName)%(Extension) diff --git a/benchmarks/Microsoft.AspNetCore.Razor.Performance/TagHelperSerializationBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Razor.Performance/TagHelperSerializationBenchmark.cs index bfee04164f..f1dcabe640 100644 --- a/benchmarks/Microsoft.AspNetCore.Razor.Performance/TagHelperSerializationBenchmark.cs +++ b/benchmarks/Microsoft.AspNetCore.Razor.Performance/TagHelperSerializationBenchmark.cs @@ -7,7 +7,7 @@ using System.IO; using System.Text; using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.VisualStudio.LanguageServices.Razor; +using Microsoft.VisualStudio.LanguageServices.Razor.Serialization; using Newtonsoft.Json; namespace Microsoft.AspNetCore.Razor.Performance diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/ExtensionInitializer.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/ExtensionInitializer.cs new file mode 100644 index 0000000000..51f16b552f --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/ExtensionInitializer.cs @@ -0,0 +1,29 @@ +// 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.AspNetCore.Mvc.Razor.Extensions.Version1_X +{ + internal class ExtensionInitializer : RazorExtensionInitializer + { + public override void Initialize(RazorProjectEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (builder.Configuration.ConfigurationName == "MVC-1.0") + { + RazorExtensions.Register(builder); + } + else if (builder.Configuration.ConfigurationName == "MVC-1.1") + { + RazorExtensions.Register(builder); + RazorExtensions.RegisterViewComponentTagHelpers(builder); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/Properties/AssemblyInfo.cs index 8bed236c69..8233c62607 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/Properties/AssemblyInfo.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/Properties/AssemblyInfo.cs @@ -2,5 +2,10 @@ // 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.Version1_X; +using Microsoft.AspNetCore.Razor.Language; + +[assembly: ProvideRazorExtensionInitializer("MVC-1.0", typeof(ExtensionInitializer))] +[assembly: ProvideRazorExtensionInitializer("MVC-1.1", typeof(ExtensionInitializer))] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] 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 e8b4f0018c..18bfe99a61 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/RazorExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/RazorExtensions.cs @@ -45,6 +45,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X throw new ArgumentNullException(nameof(builder)); } + builder.Features.Add(new ViewComponentTagHelperDescriptorProvider()); + builder.Features.Add(new ViewComponentTagHelperPass()); builder.AddTargetExtension(new ViewComponentTagHelperTargetExtension()); } @@ -88,6 +90,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X EnsureDesignTime(builder); + builder.Features.Add(new ViewComponentTagHelperDescriptorProvider()); builder.Features.Add(new ViewComponentTagHelperPass()); builder.AddTargetExtension(new ViewComponentTagHelperTargetExtension()); } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/ViewComponentTypeVisitor.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/ViewComponentTypeVisitor.cs index fe70827712..90cb1eedca 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/ViewComponentTypeVisitor.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/ViewComponentTypeVisitor.cs @@ -24,10 +24,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X var symbol = compilation.GetAssemblyOrModuleSymbol(reference) as IAssemblySymbol; if (symbol != null) { - if (string.Equals(symbol.Identity.Name, ViewComponentTypes.Assembly, StringComparison.Ordinal) && - symbol.Identity.Version > ViewComponentTypes.AssemblyVersion) + if (string.Equals(symbol.Identity.Name, ViewComponentTypes.Assembly, StringComparison.Ordinal)) { - enabled = true; + enabled = symbol.Identity.Version >= ViewComponentTypes.AssemblyVersion; break; } } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/RazorExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/RazorExtensions.cs index abf1b447f0..589798fffa 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/RazorExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/RazorExtensions.cs @@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions FunctionsDirective.Register(builder); InheritsDirective.Register(builder); SectionDirective.Register(builder); - + builder.Features.Add(new ViewComponentTagHelperDescriptorProvider()); builder.AddTargetExtension(new ViewComponentTagHelperTargetExtension()); @@ -61,6 +61,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() { diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ViewComponentTypeVisitor.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ViewComponentTypeVisitor.cs index c26779a9c5..9b44634095 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ViewComponentTypeVisitor.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ViewComponentTypeVisitor.cs @@ -10,31 +10,14 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions { internal class ViewComponentTypeVisitor : SymbolVisitor { - private static readonly Version SupportedVCTHMvcVersion = new Version(1, 1); - private readonly INamedTypeSymbol _viewComponentAttribute; private readonly INamedTypeSymbol _nonViewComponentAttribute; private readonly List _results; public static ViewComponentTypeVisitor Create(Compilation compilation, List results) { - var enabled = false; - foreach (var reference in compilation.References) - { - var symbol = compilation.GetAssemblyOrModuleSymbol(reference) as IAssemblySymbol; - if (symbol != null) - { - if (string.Equals(symbol.Identity.Name, ViewComponentTypes.Assembly, StringComparison.Ordinal) && - symbol.Identity.Version > ViewComponentTypes.AssemblyVersion) - { - enabled = true; - break; - } - } - } - - var vcAttribute = enabled ? compilation.GetTypeByMetadataName(ViewComponentTypes.ViewComponentAttribute) : null; - var nonVCAttribute = enabled ? compilation.GetTypeByMetadataName(ViewComponentTypes.NonViewComponentAttribute) : null; + var vcAttribute = compilation.GetTypeByMetadataName(ViewComponentTypes.ViewComponentAttribute); + var nonVCAttribute = compilation.GetTypeByMetadataName(ViewComponentTypes.NonViewComponentAttribute); return new ViewComponentTypeVisitor(vcAttribute, nonVCAttribute, results); } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/FallbackProjectEngineFactory.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/FallbackProjectEngineFactory.cs new file mode 100644 index 0000000000..855acedde2 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/FallbackProjectEngineFactory.cs @@ -0,0 +1,37 @@ +// 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; +using Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.CodeAnalysis.Razor +{ + [Export(typeof(IFallbackProjectEngineFactory))] + internal class FallbackProjectEngineFactory : IFallbackProjectEngineFactory + { + public RazorProjectEngine Create(RazorConfiguration configuration, RazorProjectFileSystem fileSystem, Action configure) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + if (fileSystem == null) + { + throw new ArgumentNullException(nameof(fileSystem)); + } + + // This is a very basic implementation that will provide reasonable support without crashing. + // If the user falls into this situation, ideally they can realize that something is wrong and take + // action. + // + // This has no support for: + // - Tag Helpers + // - Imports + // - Default Imports + // - and will have a very limited set of directives + return RazorProjectEngine.Create(configuration, fileSystem, configure); + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/IFallbackProjectEngineFactory.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/IFallbackProjectEngineFactory.cs new file mode 100644 index 0000000000..ccdc1b80c1 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/IFallbackProjectEngineFactory.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.CodeAnalysis.Razor +{ + // Used to create the 'fallback' project engine when we don't have a custom implementation. + internal interface IFallbackProjectEngineFactory : IProjectEngineFactory + { + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerExtensions.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerExtensions.cs index 579a42ffcb..d6299717ca 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerExtensions.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerExtensions.cs @@ -13,7 +13,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem for (var i = 0; i< projects.Count; i++) { var project = projects[i]; - if (string.Equals(filePath, project.WorkspaceProject.FilePath, StringComparison.OrdinalIgnoreCase)) + if (FilePathComparer.Instance.Equals(filePath, project.FilePath)) { return project; } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/Properties/AssemblyInfo.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/Properties/AssemblyInfo.cs index a73900fa42..d6cf7bdc7e 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/Properties/AssemblyInfo.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/Properties/AssemblyInfo.cs @@ -2,7 +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.CodeAnalysis.Razor.Workspaces.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.CodeAnalysis.Remote.Razor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.VisualStudio.Editor.Razor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorProjectEngineFactoryService.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorProjectEngineFactoryService.cs index 0388de4671..4defb21947 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorProjectEngineFactoryService.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorProjectEngineFactoryService.cs @@ -4,11 +4,20 @@ using System; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; namespace Microsoft.CodeAnalysis.Razor { internal abstract class RazorProjectEngineFactoryService : ILanguageService { - public abstract RazorProjectEngine Create(string projectPath, Action configure); + public abstract IProjectEngineFactory FindFactory(ProjectSnapshot project); + + public abstract IProjectEngineFactory FindSerializableFactory(ProjectSnapshot project); + + public abstract RazorProjectEngine Create(ProjectSnapshot project, Action configure); + + public abstract RazorProjectEngine Create(ProjectSnapshot project, RazorProjectFileSystem fileSystem, Action configure); + + public abstract RazorProjectEngine Create(string directoryPath, Action configure); } } \ No newline at end of file diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolutionResult.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolutionResult.cs index 21b553cde8..0e8c7b19ef 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolutionResult.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolutionResult.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; using System.Collections.Generic; using Microsoft.AspNetCore.Razor.Language; @@ -8,6 +9,8 @@ namespace Microsoft.CodeAnalysis.Razor { public sealed class TagHelperResolutionResult { + internal static TagHelperResolutionResult Empty = new TagHelperResolutionResult(Array.Empty(), Array.Empty()); + public TagHelperResolutionResult(IReadOnlyList descriptors, IReadOnlyList diagnostics) { Descriptors = descriptors; diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolver.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolver.cs index 182273abfd..d17dc90157 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolver.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolver.cs @@ -1,14 +1,57 @@ // 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; using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; namespace Microsoft.CodeAnalysis.Razor { internal abstract class TagHelperResolver : ILanguageService { - public abstract Task GetTagHelpersAsync(Project project, CancellationToken cancellationToken); + public abstract Task GetTagHelpersAsync(ProjectSnapshot project, CancellationToken cancellationToken = default); + + protected virtual async Task GetTagHelpersAsync(ProjectSnapshot project, RazorProjectEngine engine) + { + if (project == null) + { + throw new ArgumentNullException(nameof(project)); + } + + if (engine == null) + { + throw new ArgumentNullException(nameof(engine)); + } + + if (project.WorkspaceProject == null) + { + return TagHelperResolutionResult.Empty; + } + + var providers = engine.Engine.Features.OfType().ToArray(); + if (providers.Length == 0) + { + return TagHelperResolutionResult.Empty; + } + + var results = new List(); + var context = TagHelperDescriptorProviderContext.Create(results); + + var compilation = await project.WorkspaceProject.GetCompilationAsync().ConfigureAwait(false); + context.SetCompilation(compilation); + + for (var i = 0; i < providers.Length; i++) + { + var provider = providers[i]; + provider.Execute(context); + } + + return new TagHelperResolutionResult(results, Array.Empty()); + } } } \ No newline at end of file diff --git a/src/Microsoft.CodeAnalysis.Remote.Razor/DefaultTagHelperResolver.cs b/src/Microsoft.CodeAnalysis.Remote.Razor/DefaultTagHelperResolver.cs deleted file mode 100644 index 4aca5c8f7e..0000000000 --- a/src/Microsoft.CodeAnalysis.Remote.Razor/DefaultTagHelperResolver.cs +++ /dev/null @@ -1,54 +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.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.Razor.Extensions; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.CodeAnalysis.Razor; - -namespace Microsoft.CodeAnalysis.Remote.Razor -{ - internal class DefaultTagHelperResolver : TagHelperResolver - { - public DefaultTagHelperResolver(bool designTime) - { - DesignTime = designTime; - } - - public bool DesignTime { get; } - - private TagHelperResolutionResult GetTagHelpers(Compilation compilation) - { - var descriptors = new List(); - - var providers = new ITagHelperDescriptorProvider[] - { - new DefaultTagHelperDescriptorProvider() { DesignTime = DesignTime, }, - new ViewComponentTagHelperDescriptorProvider(), - }; - - var results = new List(); - var context = TagHelperDescriptorProviderContext.Create(results); - context.SetCompilation(compilation); - - for (var i = 0; i < providers.Length; i++) - { - var provider = providers[i]; - provider.Execute(context); - } - - var diagnostics = new List(); - var resolutionResult = new TagHelperResolutionResult(results, diagnostics); - - return resolutionResult; - } - - public override async Task GetTagHelpersAsync(Project project, CancellationToken cancellationToken) - { - var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); - return GetTagHelpers(compilation); - } - } -} diff --git a/src/Microsoft.CodeAnalysis.Remote.Razor/Microsoft.CodeAnalysis.Remote.Razor.csproj b/src/Microsoft.CodeAnalysis.Remote.Razor/Microsoft.CodeAnalysis.Remote.Razor.csproj index 7cf58777ef..369dd82020 100644 --- a/src/Microsoft.CodeAnalysis.Remote.Razor/Microsoft.CodeAnalysis.Remote.Razor.csproj +++ b/src/Microsoft.CodeAnalysis.Remote.Razor/Microsoft.CodeAnalysis.Remote.Razor.csproj @@ -7,12 +7,13 @@ - + + Serialization\%(FileName)%(Extension) + - diff --git a/src/Microsoft.CodeAnalysis.Remote.Razor/RazorLanguageService.cs b/src/Microsoft.CodeAnalysis.Remote.Razor/RazorLanguageService.cs index 930abdee3a..f0bf9bdaf8 100644 --- a/src/Microsoft.CodeAnalysis.Remote.Razor/RazorLanguageService.cs +++ b/src/Microsoft.CodeAnalysis.Remote.Razor/RazorLanguageService.cs @@ -10,34 +10,22 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Razor; -using Microsoft.VisualStudio.LanguageServices.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; namespace Microsoft.CodeAnalysis.Remote.Razor { - internal class RazorLanguageService : ServiceHubServiceBase + internal class RazorLanguageService : RazorServiceBase { public RazorLanguageService(Stream stream, IServiceProvider serviceProvider) - : base(serviceProvider, stream) + : base(stream, serviceProvider) { - Rpc.JsonSerializer.Converters.Add(new RazorDiagnosticJsonConverter()); - - // Due to this issue - https://github.com/dotnet/roslyn/issues/16900#issuecomment-277378950 - // We need to manually start the RPC connection. Otherwise we'd be opting ourselves into - // race condition prone call paths. - Rpc.StartListening(); } - public async Task GetTagHelpersAsync(Guid projectIdBytes, string projectDebugName, CancellationToken cancellationToken = default(CancellationToken)) + public async Task GetTagHelpersAsync(ProjectSnapshotHandle projectHandle, string factoryTypeName, CancellationToken cancellationToken = default) { - var projectId = ProjectId.CreateFromSerialized(projectIdBytes, projectDebugName); + var project = await GetProjectSnapshotAsync(projectHandle, cancellationToken).ConfigureAwait(false); - var solution = await GetSolutionAsync(cancellationToken).ConfigureAwait(false); - var project = solution.GetProject(projectId); - - var resolver = new DefaultTagHelperResolver(designTime: true); - var result = await resolver.GetTagHelpersAsync(project, cancellationToken).ConfigureAwait(false); - - return result; + return await RazorServices.TagHelperResolver.GetTagHelpersAsync(project, factoryTypeName, cancellationToken); } public Task> GetDirectivesAsync(Guid projectIdBytes, string projectDebugName, CancellationToken cancellationToken = default(CancellationToken)) diff --git a/src/Microsoft.CodeAnalysis.Remote.Razor/RazorServiceBase.cs b/src/Microsoft.CodeAnalysis.Remote.Razor/RazorServiceBase.cs new file mode 100644 index 0000000000..6f93dbcdbf --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Remote.Razor/RazorServiceBase.cs @@ -0,0 +1,67 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; + +namespace Microsoft.CodeAnalysis.Remote.Razor +{ + internal abstract class RazorServiceBase : ServiceHubServiceBase + { + public RazorServiceBase(Stream stream, IServiceProvider serviceProvider) + : base(serviceProvider, stream) + { + RazorServices = new RazorServices(); + + Rpc.JsonSerializer.Converters.RegisterRazorConverters(); + + // Due to this issue - https://github.com/dotnet/roslyn/issues/16900#issuecomment-277378950 + // We need to manually start the RPC connection. Otherwise we'd be opting ourselves into + // race condition prone call paths. + Rpc.StartListening(); + } + + protected RazorServices RazorServices { get; } + + protected virtual async Task GetProjectSnapshotAsync(ProjectSnapshotHandle projectHandle, CancellationToken cancellationToken) + { + if (projectHandle == null) + { + throw new ArgumentNullException(nameof(projectHandle)); + } + + var solution = await GetSolutionAsync(cancellationToken).ConfigureAwait(false); + var workspaceProject = solution.GetProject(projectHandle.WorkspaceProjectId); + + return new SerializedProjectSnapshot(projectHandle.FilePath, projectHandle.Configuration, workspaceProject); + } + + private class SerializedProjectSnapshot : ProjectSnapshot + { + public SerializedProjectSnapshot(string filePath, RazorConfiguration configuration, Project workspaceProject) + { + FilePath = filePath; + Configuration = configuration; + WorkspaceProject = workspaceProject; + + IsInitialized = true; + Version = VersionStamp.Default; + } + + public override RazorConfiguration Configuration { get; } + + public override string FilePath { get; } + + public override bool IsInitialized { get; } + + public override VersionStamp Version { get; } + + public override Project WorkspaceProject { get; } + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Remote.Razor/RazorServices.cs b/src/Microsoft.CodeAnalysis.Remote.Razor/RazorServices.cs new file mode 100644 index 0000000000..d3a60a400c --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Remote.Razor/RazorServices.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.CodeAnalysis.Razor +{ + // Provides access to Razor language and workspace services that are avialable in the OOP host. + // + // Since we don't have access to the workspace we only have access to some specific things + // that we can construct directly. + internal class RazorServices + { + public RazorServices() + { + FallbackProjectEngineFactory = new FallbackProjectEngineFactory(); + TagHelperResolver = new RemoteTagHelperResolver(FallbackProjectEngineFactory); + } + + public IFallbackProjectEngineFactory FallbackProjectEngineFactory { get; } + + public RemoteTagHelperResolver TagHelperResolver { get; } + } +} diff --git a/src/Microsoft.CodeAnalysis.Remote.Razor/RemoteTagHelperResolver.cs b/src/Microsoft.CodeAnalysis.Remote.Razor/RemoteTagHelperResolver.cs new file mode 100644 index 0000000000..5c8697db7f --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Remote.Razor/RemoteTagHelperResolver.cs @@ -0,0 +1,84 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; + +namespace Microsoft.CodeAnalysis.Razor +{ + internal class RemoteTagHelperResolver : TagHelperResolver + { + private readonly static RazorConfiguration DefaultConfiguration = FallbackRazorConfiguration.MVC_2_0; + + private readonly IFallbackProjectEngineFactory _fallbackFactory; + + public RemoteTagHelperResolver(IFallbackProjectEngineFactory fallbackFactory) + { + if (fallbackFactory == null) + { + throw new ArgumentNullException(nameof(fallbackFactory)); + } + + _fallbackFactory = fallbackFactory; + } + + public override Task GetTagHelpersAsync(ProjectSnapshot project, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public Task GetTagHelpersAsync(ProjectSnapshot project, string factoryTypeName, CancellationToken cancellationToken = default) + { + if (project == null) + { + throw new ArgumentNullException(nameof(project)); + } + + if (project.Configuration == null || project.WorkspaceProject == null) + { + return Task.FromResult(TagHelperResolutionResult.Empty); + } + + var engine = CreateProjectEngine(project, factoryTypeName); + return GetTagHelpersAsync(project, engine); + } + + internal RazorProjectEngine CreateProjectEngine(ProjectSnapshot project, string factoryTypeName) + { + // This section is really similar to the code DefaultProjectEngineFactoryService + // but with a few differences that are significant in the remote scenario + // + // Most notably, we are going to find the Tag Helpers using a compilation, and we have + // no editor settings. + Action configure = (b) => + { + b.Features.Add(new DefaultTagHelperDescriptorProvider() { DesignTime = true }); + }; + + // The default configuration currently matches MVC-2.0. Beyond MVC-2.0 we added SDK support for + // properly detecting project versions, so that's a good version to assume when we can't find a + // configuration. + var configuration = project?.Configuration ?? DefaultConfiguration; + + // If there's no factory to handle the configuration then fall back to a very basic configuration. + // + // This will stop a crash from happening in this case (misconfigured project), but will still make + // it obvious to the user that something is wrong. + var factory = CreateFactory(configuration, factoryTypeName) ?? _fallbackFactory; + return factory.Create(configuration, RazorProjectFileSystem.Empty, configure); + } + + private IProjectEngineFactory CreateFactory(RazorConfiguration configuration, string factoryTypeName) + { + if (factoryTypeName == null) + { + return null; + } + + return (IProjectEngineFactory)Activator.CreateInstance(Type.GetType(factoryTypeName, throwOnError: true)); + } + } +} diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryService.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryService.cs index 4f949d7ecd..69034198bd 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryService.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryService.cs @@ -6,8 +6,6 @@ using System.IO; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Mvc1_X = Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X; -using MvcLatest = Microsoft.AspNetCore.Mvc.Razor.Extensions; namespace Microsoft.VisualStudio.Editor.Razor { @@ -16,58 +14,127 @@ namespace Microsoft.VisualStudio.Editor.Razor private readonly static RazorConfiguration DefaultConfiguration = FallbackRazorConfiguration.MVC_2_0; private readonly ProjectSnapshotManager _projectManager; + private readonly IFallbackProjectEngineFactory _defaultFactory; + private readonly Lazy[] _customFactories; - public DefaultProjectEngineFactoryService(ProjectSnapshotManager projectManager) + public DefaultProjectEngineFactoryService( + ProjectSnapshotManager projectManager, + IFallbackProjectEngineFactory defaultFactory, + Lazy[] customFactories) { if (projectManager == null) { throw new ArgumentNullException(nameof(projectManager)); } + if (defaultFactory == null) + { + throw new ArgumentNullException(nameof(defaultFactory)); + } + + if (customFactories == null) + { + throw new ArgumentNullException(nameof(customFactories)); + } + _projectManager = projectManager; + _defaultFactory = defaultFactory; + _customFactories = customFactories; } - public override RazorProjectEngine Create(string projectPath, Action configure) + public override IProjectEngineFactory FindFactory(ProjectSnapshot project) { - if (projectPath == null) + if (project == null) { - throw new ArgumentNullException(nameof(projectPath)); + throw new ArgumentNullException(nameof(project)); } - // In 15.5 we expect projectPath to be a directory, NOT the path to the csproj. - var project = FindProject(projectPath); - var configuration = project?.Configuration ?? DefaultConfiguration; - var fileSystem = RazorProjectFileSystem.Create(projectPath); - - RazorProjectEngine projectEngine; - if (configuration.LanguageVersion.Major == 1) - { - projectEngine = RazorProjectEngine.Create(configuration, fileSystem, b => - { - configure?.Invoke(b); - - Mvc1_X.RazorExtensions.Register(b); - - if (configuration.LanguageVersion.Minor >= 1) - { - Mvc1_X.RazorExtensions.RegisterViewComponentTagHelpers(b); - } - }); - } - else - { - projectEngine = RazorProjectEngine.Create(configuration, fileSystem, b => - { - configure?.Invoke(b); - - MvcLatest.RazorExtensions.Register(b); - }); - } - - return projectEngine; + return SelectFactory(project.Configuration ?? DefaultConfiguration, requireSerializable: false); } - private ProjectSnapshot FindProject(string directory) + public override IProjectEngineFactory FindSerializableFactory(ProjectSnapshot project) + { + if (project == null) + { + throw new ArgumentNullException(nameof(project)); + } + + return SelectFactory(project.Configuration ?? DefaultConfiguration, requireSerializable: true); + } + + public override RazorProjectEngine Create(ProjectSnapshot project, Action configure) + { + if (project == null) + { + throw new ArgumentNullException(nameof(project)); + } + + return CreateCore(project, RazorProjectFileSystem.Create(Path.GetDirectoryName(project.FilePath)), configure); + } + + public override RazorProjectEngine Create(string directoryPath, Action configure) + { + if (directoryPath == null) + { + throw new ArgumentNullException(nameof(directoryPath)); + } + + var project = FindProjectByDirectory(directoryPath); + return CreateCore(project, RazorProjectFileSystem.Create(directoryPath), configure); + } + + public override RazorProjectEngine Create(ProjectSnapshot project, RazorProjectFileSystem fileSystem, Action configure) + { + if (project == null) + { + throw new ArgumentNullException(nameof(project)); + } + + if (fileSystem == null) + { + throw new ArgumentNullException(nameof(fileSystem)); + } + + return CreateCore(project, fileSystem, configure); + } + + private RazorProjectEngine CreateCore(ProjectSnapshot project, RazorProjectFileSystem fileSystem, Action configure) + { + // When we're running in the editor, the editor provides a configure delegate that will include + // the editor settings and tag helpers. + // + // This service is only used in process in Visual Studio, and any other callers should provide these + // things also. + configure = configure ?? ((b) => { }); + + // The default configuration currently matches MVC-2.0. Beyond MVC-2.0 we added SDK support for + // properly detecting project versions, so that's a good version to assume when we can't find a + // configuration. + var configuration = project?.Configuration ?? DefaultConfiguration; + + // If there's no factory to handle the configuration then fall back to a very basic configuration. + // + // This will stop a crash from happening in this case (misconfigured project), but will still make + // it obvious to the user that something is wrong. + var factory = SelectFactory(configuration) ?? _defaultFactory; + return factory.Create(configuration, fileSystem, configure); + } + + private IProjectEngineFactory SelectFactory(RazorConfiguration configuration, bool requireSerializable = false) + { + for (var i = 0; i < _customFactories.Length; i++) + { + var factory = _customFactories[i]; + if (string.Equals(configuration.ConfigurationName, factory.Metadata.ConfigurationName)) + { + return requireSerializable && !factory.Metadata.SupportsSerialization ? null : factory.Value; + } + } + + return null; + } + + private ProjectSnapshot FindProjectByDirectory(string directory) { directory = NormalizeDirectoryPath(directory); @@ -75,9 +142,9 @@ namespace Microsoft.VisualStudio.Editor.Razor for (var i = 0; i < projects.Count; i++) { var project = projects[i]; - if (project.WorkspaceProject?.FilePath != null) + if (project.FilePath != null) { - if (string.Equals(directory, NormalizeDirectoryPath(Path.GetDirectoryName(project.WorkspaceProject.FilePath)), StringComparison.OrdinalIgnoreCase)) + if (string.Equals(directory, NormalizeDirectoryPath(Path.GetDirectoryName(project.FilePath)), StringComparison.OrdinalIgnoreCase)) { return project; } diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryServiceFactory.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryServiceFactory.cs index babed97a5c..13617d03b5 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryServiceFactory.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryServiceFactory.cs @@ -1,6 +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. +using System; +using System.Collections.Generic; +using System.Composition; +using System.Linq; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Razor; @@ -11,9 +15,34 @@ namespace Microsoft.VisualStudio.Editor.Razor [ExportLanguageServiceFactory(typeof(RazorProjectEngineFactoryService), RazorLanguage.Name, ServiceLayer.Default)] internal class DefaultProjectEngineFactoryServiceFactory : ILanguageServiceFactory { + private readonly Lazy[] _customFactories; + private readonly IFallbackProjectEngineFactory _fallbackFactory; + + [ImportingConstructor] + public DefaultProjectEngineFactoryServiceFactory( + IFallbackProjectEngineFactory fallbackFactory, + [ImportMany] IEnumerable> customFactories) + { + if (fallbackFactory == null) + { + throw new ArgumentNullException(nameof(fallbackFactory)); + } + + if (customFactories == null) + { + throw new ArgumentNullException(nameof(customFactories)); + } + + _fallbackFactory = fallbackFactory; + _customFactories = customFactories.ToArray(); + } + public ILanguageService CreateLanguageService(HostLanguageServices languageServices) { - return new DefaultProjectEngineFactoryService(languageServices.GetRequiredService()); + return new DefaultProjectEngineFactoryService( + languageServices.GetRequiredService(), + _fallbackFactory, + _customFactories); } } } \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolver.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolver.cs index 7547bde441..b303f7da60 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolver.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolver.cs @@ -2,59 +2,45 @@ // 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.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.Razor.Extensions; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; namespace Microsoft.VisualStudio.Editor.Razor { internal class DefaultTagHelperResolver : TagHelperResolver { - // Hack for testability. The view component visitor will normally just no op if we're not referencing - // an appropriate version of MVC. - internal bool ForceEnableViewComponentDiscovery { get; set; } + private readonly RazorProjectEngineFactoryService _engineFactory; - public override async Task GetTagHelpersAsync(Project project, CancellationToken cancellationToken) + public DefaultTagHelperResolver(RazorProjectEngineFactoryService engineFactory) + { + if (engineFactory == null) + { + throw new ArgumentNullException(nameof(engineFactory)); + } + + _engineFactory = engineFactory; + } + + public override Task GetTagHelpersAsync(ProjectSnapshot project, CancellationToken cancellationToken = default) { if (project == null) { throw new ArgumentNullException(nameof(project)); } - var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); - var result = GetTagHelpers(compilation); - return result; - } - - // Internal for testing - internal TagHelperResolutionResult GetTagHelpers(Compilation compilation) - { - var descriptors = new List(); - - var providers = new ITagHelperDescriptorProvider[] + if (project.Configuration == null || project.WorkspaceProject == null) { - new DefaultTagHelperDescriptorProvider() { DesignTime = true, }, - new ViewComponentTagHelperDescriptorProvider() { ForceEnabled = ForceEnableViewComponentDiscovery }, - }; - - var results = new List(); - var context = TagHelperDescriptorProviderContext.Create(results); - context.SetCompilation(compilation); - - for (var i = 0; i < providers.Length; i++) - { - var provider = providers[i]; - provider.Execute(context); + return Task.FromResult(TagHelperResolutionResult.Empty); } - var diagnostics = new List(); - var resolutionResult = new TagHelperResolutionResult(results, diagnostics); - - return resolutionResult; + var engine = _engineFactory.Create(project, RazorProjectFileSystem.Empty, b => + { + b.Features.Add(new DefaultTagHelperDescriptorProvider() { DesignTime = true, }); + }); + return GetTagHelpersAsync(project, engine); } } } \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolverFactory.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolverFactory.cs index 50e2e96179..1a238ad473 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolverFactory.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolverFactory.cs @@ -14,7 +14,7 @@ namespace Microsoft.VisualStudio.Editor.Razor { public ILanguageService CreateLanguageService(HostLanguageServices languageServices) { - return new DefaultTagHelperResolver(); + return new DefaultTagHelperResolver(languageServices.GetRequiredService()); } } } \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_1_0.cs b/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_1_0.cs new file mode 100644 index 0000000000..9b3b92eb89 --- /dev/null +++ b/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_1_0.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.Reflection; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Razor; + +namespace Microsoft.VisualStudio.Editor.Razor +{ + [ExportCustomProjectEngineFactory("MVC-1.0", SupportsSerialization = true)] + internal class LegacyProjectEngineFactory_1_0 : IProjectEngineFactory + { + private const string AssemblyName = "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X"; + + public RazorProjectEngine Create(RazorConfiguration configuration, RazorProjectFileSystem fileSystem, Action configure) + { + // Rewrite the assembly name into a full name just like this one, but with the name of the MVC design time assembly. + var assemblyName = new AssemblyName(typeof(LegacyProjectEngineFactory_1_0).Assembly.FullName); + assemblyName.Name = AssemblyName; + + var extension = new AssemblyExtension(configuration.ConfigurationName, Assembly.Load(assemblyName)); + var initializer = extension.CreateInitializer(); + + return RazorProjectEngine.Create(configuration, fileSystem, b => + { + initializer.Initialize(b); + configure?.Invoke(b); + }); + } + } +} diff --git a/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_1_1.cs b/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_1_1.cs new file mode 100644 index 0000000000..d01e6999a8 --- /dev/null +++ b/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_1_1.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.Reflection; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Razor; + +namespace Microsoft.VisualStudio.Editor.Razor +{ + [ExportCustomProjectEngineFactory("MVC-1.1", SupportsSerialization = true)] + internal class LegacyProjectEngineFactory_1_1 : IProjectEngineFactory + { + private const string AssemblyName = "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X"; + + public RazorProjectEngine Create(RazorConfiguration configuration, RazorProjectFileSystem fileSystem, Action configure) + { + // Rewrite the assembly name into a full name just like this one, but with the name of the MVC design time assembly. + var assemblyName = new AssemblyName(typeof(LegacyProjectEngineFactory_1_1).Assembly.FullName); + assemblyName.Name = AssemblyName; + + var extension = new AssemblyExtension(configuration.ConfigurationName, Assembly.Load(assemblyName)); + var initializer = extension.CreateInitializer(); + + return RazorProjectEngine.Create(configuration, fileSystem, b => + { + initializer.Initialize(b); + configure?.Invoke(b); + }); + } + } +} diff --git a/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_2_0.cs b/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_2_0.cs new file mode 100644 index 0000000000..ebf9e144f5 --- /dev/null +++ b/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_2_0.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Reflection; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Razor; + +namespace Microsoft.VisualStudio.Editor.Razor +{ + [ExportCustomProjectEngineFactory("MVC-2.0", SupportsSerialization = true)] + internal class LegacyProjectEngineFactory_2_0 : IProjectEngineFactory + { + private const string AssemblyName = "Microsoft.AspNetCore.Mvc.Razor.Extensions"; + public RazorProjectEngine Create(RazorConfiguration configuration, RazorProjectFileSystem fileSystem, Action configure) + { + // Rewrite the assembly name into a full name just like this one, but with the name of the MVC design time assembly. + var assemblyName = new AssemblyName(typeof(LegacyProjectEngineFactory_2_0).Assembly.FullName); + assemblyName.Name = AssemblyName; + + var extension = new AssemblyExtension(configuration.ConfigurationName, Assembly.Load(assemblyName)); + var initializer = extension.CreateInitializer(); + + return RazorProjectEngine.Create(configuration, fileSystem, b => + { + initializer.Initialize(b); + configure?.Invoke(b); + }); + } + } +} diff --git a/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_2_1.cs b/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_2_1.cs new file mode 100644 index 0000000000..df8d04667c --- /dev/null +++ b/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_2_1.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Reflection; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Razor; + +namespace Microsoft.VisualStudio.Editor.Razor +{ + // Currently we provide a fixed configuration for 2.1, but this is a point-in-time issue. We plan + // to make the 2.1 configuration more flexible and less hardcoded. + [ExportCustomProjectEngineFactory("MVC-2.1", SupportsSerialization = true)] + internal class LegacyProjectEngineFactory_2_1 : IProjectEngineFactory + { + private const string AssemblyName = "Microsoft.AspNetCore.Mvc.Razor.Extensions"; + public RazorProjectEngine Create(RazorConfiguration configuration, RazorProjectFileSystem fileSystem, Action configure) + { + // Rewrite the assembly name into a full name just like this one, but with the name of the MVC design time assembly. + var assemblyName = new AssemblyName(typeof(LegacyProjectEngineFactory_2_1).Assembly.FullName); + assemblyName.Name = AssemblyName; + + var extension = new AssemblyExtension(configuration.ConfigurationName, Assembly.Load(assemblyName)); + var initializer = extension.CreateInitializer(); + + return RazorProjectEngine.Create(configuration, fileSystem, b => + { + initializer.Initialize(b); + configure?.Invoke(b); + }); + } + } +} diff --git a/src/Microsoft.VisualStudio.Editor.Razor/Microsoft.VisualStudio.Editor.Razor.csproj b/src/Microsoft.VisualStudio.Editor.Razor/Microsoft.VisualStudio.Editor.Razor.csproj index 32b484f542..445cb0e354 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/Microsoft.VisualStudio.Editor.Razor.csproj +++ b/src/Microsoft.VisualStudio.Editor.Razor/Microsoft.VisualStudio.Editor.Razor.csproj @@ -14,8 +14,6 @@ - - diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/LegacyTagHelperResolver.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/LegacyTagHelperResolver.cs index 54b67052e2..ecd4d9e256 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/LegacyTagHelperResolver.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/LegacyTagHelperResolver.cs @@ -1,11 +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.ComponentModel.Composition; -using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; namespace Microsoft.VisualStudio.LanguageServices.Razor { @@ -14,23 +15,42 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor // use TagHelperResolver. // ---------------------------------------------------------------------------------------------------- [Export(typeof(ITagHelperResolver))] - internal class LegacyTagHelperResolver : OOPTagHelperResolver, ITagHelperResolver + internal class LegacyTagHelperResolver : ITagHelperResolver { + private readonly Workspace _workspace; + [ImportingConstructor] - public LegacyTagHelperResolver( - [Import(typeof(VisualStudioWorkspace))] Workspace workspace) - : base(workspace) + public LegacyTagHelperResolver([Import(typeof(VisualStudioWorkspace))] Workspace workspace) { + if (workspace == null) + { + throw new ArgumentNullException(nameof(workspace)); + } + + _workspace = workspace; } public Task GetTagHelpersAsync(Project project) { if (project == null) { - throw new System.ArgumentNullException(nameof(project)); + throw new ArgumentNullException(nameof(project)); } - return base.GetTagHelpersAsync(project, CancellationToken.None); + if (project.FilePath == null) + { + return Task.FromResult(TagHelperResolutionResult.Empty); + } + + var projectManager = _workspace.Services.GetLanguageServices(RazorLanguage.Name).GetRequiredService(); + var projectSnapshot = projectManager.GetProjectWithFilePath(project.FilePath); + if (projectSnapshot == null) + { + return Task.FromResult(TagHelperResolutionResult.Empty); + } + + var resolver = _workspace.Services.GetLanguageServices(RazorLanguage.Name).GetRequiredService(); + return resolver.GetTagHelpersAsync(projectSnapshot); } } } \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/OOPTagHelperResolver.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/OOPTagHelperResolver.cs index f0e81aeece..130d148baa 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/OOPTagHelperResolver.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/OOPTagHelperResolver.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.VisualStudio.Editor.Razor; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -15,53 +16,67 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor internal class OOPTagHelperResolver : TagHelperResolver { private readonly DefaultTagHelperResolver _defaultResolver; + private readonly RazorProjectEngineFactoryService _engineFactory; + private readonly ErrorReporter _errorReporter; private readonly Workspace _workspace; - public OOPTagHelperResolver(Workspace workspace) + public OOPTagHelperResolver(RazorProjectEngineFactoryService engineFactory, ErrorReporter errorReporter, Workspace workspace) { + if (engineFactory == null) + { + throw new ArgumentNullException(nameof(engineFactory)); + } + + if (errorReporter == null) + { + throw new ArgumentNullException(nameof(errorReporter)); + } + if (workspace == null) { throw new ArgumentNullException(nameof(workspace)); } + _engineFactory = engineFactory; + _errorReporter = errorReporter; _workspace = workspace; - _defaultResolver = new DefaultTagHelperResolver(); + + _defaultResolver = new DefaultTagHelperResolver(_engineFactory); } - public override async Task GetTagHelpersAsync(Project project, CancellationToken cancellationToken) + public override async Task GetTagHelpersAsync(ProjectSnapshot project, CancellationToken cancellationToken = default) { if (project == null) { throw new ArgumentNullException(nameof(project)); } + if (project.Configuration == null || project.WorkspaceProject == null) + { + return TagHelperResolutionResult.Empty; + } + + // Not every custom factory supports the OOP host. Our priority system should work like this: + // + // 1. Use custom factory out of process + // 2. Use custom factory in process + // 3. Use fallback factory in process + // + // Calling into RazorTemplateEngineFactoryService.Create will accomplish #2 and #3 in one step. + var factory = _engineFactory.FindSerializableFactory(project); + try { TagHelperResolutionResult result = null; - - // We're being defensive here because the OOP host can return null for the client/session/operation - // when it's disconnected (user stops the process). - var client = await RazorLanguageServiceClientFactory.CreateAsync(_workspace, cancellationToken); - if (client != null) + if (factory != null) { - using (var session = await client.CreateSessionAsync(project.Solution)) - { - if (session != null) - { - var jsonObject = await session.InvokeAsync( - "GetTagHelpersAsync", - new object[] { project.Id.Id, "Foo", }, - cancellationToken).ConfigureAwait(false); - - result = GetTagHelperResolutionResult(jsonObject); - } - } + result = await ResolveTagHelpersOutOfProcessAsync(factory, project); } if (result == null) { // Was unable to get tag helpers OOP, fallback to default behavior. - result = await _defaultResolver.GetTagHelpersAsync(project, cancellationToken); + result = await ResolveTagHelpersInProcessAsync(project); } return result; @@ -76,11 +91,61 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor } } - private TagHelperResolutionResult GetTagHelperResolutionResult(JObject jsonObject) + protected virtual async Task ResolveTagHelpersOutOfProcessAsync(IProjectEngineFactory factory, ProjectSnapshot project) + { + // We're being overly defensive here because the OOP host can return null for the client/session/operation + // when it's disconnected (user stops the process). + // + // This will change in the future to an easier to consume API but for VS RTM this is what we have. + try + { + var client = await RazorLanguageServiceClientFactory.CreateAsync(_workspace, CancellationToken.None); + if (client != null) + { + using (var session = await client.CreateSessionAsync(project.WorkspaceProject.Solution)) + { + if (session != null) + { + var args = new object[] + { + Serialize(project), + factory == null ? null : factory.GetType().AssemblyQualifiedName, + }; + + var json = await session.InvokeAsync("GetTagHelpersAsync", args, CancellationToken.None).ConfigureAwait(false); + return Deserialize(json); + } + } + } + } + catch (Exception ex) + { + // We silence exceptions from the OOP host because we don't want to bring down VS for an OOP failure. + // We will retry all failures in process anyway, so if there's a real problem that isn't unique to OOP + // then it will report a crash in VS. + _errorReporter.ReportError(ex, project); + } + + return null; + } + + protected virtual Task ResolveTagHelpersInProcessAsync(ProjectSnapshot project) + { + return _defaultResolver.GetTagHelpersAsync(project); + } + + private JObject Serialize(ProjectSnapshot snapshot) { var serializer = new JsonSerializer(); - serializer.Converters.Add(TagHelperDescriptorJsonConverter.Instance); - serializer.Converters.Add(RazorDiagnosticJsonConverter.Instance); + serializer.Converters.RegisterRazorConverters(); + + return JObject.FromObject(snapshot, serializer); + } + + private TagHelperResolutionResult Deserialize(JObject jsonObject) + { + var serializer = new JsonSerializer(); + serializer.Converters.RegisterRazorConverters(); using (var reader = jsonObject.CreateReader()) { diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/OOPTagHelperResolverFactory.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/OOPTagHelperResolverFactory.cs index d3265f7778..b6b4e93d52 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/OOPTagHelperResolverFactory.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/OOPTagHelperResolverFactory.cs @@ -14,8 +14,10 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor { public ILanguageService CreateLanguageService(HostLanguageServices languageServices) { - var workspace = languageServices.WorkspaceServices.Workspace; - return new OOPTagHelperResolver(workspace); + return new OOPTagHelperResolver( + languageServices.GetRequiredService(), + languageServices.WorkspaceServices.GetRequiredService(), + languageServices.WorkspaceServices.Workspace); } } } \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/JsonConverterCollectionExtensions.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/JsonConverterCollectionExtensions.cs new file mode 100644 index 0000000000..1f008fd63d --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/JsonConverterCollectionExtensions.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.VisualStudio.LanguageServices.Razor.Serialization; +using Newtonsoft.Json; + +namespace Microsoft.CodeAnalysis.Razor +{ + internal static class JsonConverterCollectionExtensions + { + public static void RegisterRazorConverters(this JsonConverterCollection collection) + { + if (collection == null) + { + throw new ArgumentNullException(nameof(collection)); + } + + collection.Add(TagHelperDescriptorJsonConverter.Instance); + collection.Add(RazorDiagnosticJsonConverter.Instance); + collection.Add(RazorExtensionJsonConverter.Instance); + collection.Add(RazorConfigurationJsonConverter.Instance); + collection.Add(ProjectSnapshotJsonConverter.Instance); + collection.Add(ProjectSnapshotHandleJsonConverter.Instance); + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/ProjectSnapshotHandle.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/ProjectSnapshotHandle.cs new file mode 100644 index 0000000000..d186db2ec8 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/ProjectSnapshotHandle.cs @@ -0,0 +1,29 @@ +// 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.ProjectSystem +{ + internal sealed class ProjectSnapshotHandle + { + public ProjectSnapshotHandle(string filePath, RazorConfiguration configuration, ProjectId workspaceProjectId) + { + if (filePath == null) + { + throw new ArgumentNullException(nameof(filePath)); + } + + FilePath = filePath; + Configuration = configuration; + WorkspaceProjectId = workspaceProjectId; + } + + public RazorConfiguration Configuration { get; } + + public string FilePath { get; } + + public ProjectId WorkspaceProjectId { get; } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/ProjectSnapshotHandleJsonConverter.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/ProjectSnapshotHandleJsonConverter.cs new file mode 100644 index 0000000000..5b72763912 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/ProjectSnapshotHandleJsonConverter.cs @@ -0,0 +1,73 @@ +// 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; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Serialization +{ + internal class ProjectSnapshotHandleJsonConverter : JsonConverter + { + public static readonly ProjectSnapshotHandleJsonConverter Instance = new ProjectSnapshotHandleJsonConverter(); + + public override bool CanConvert(Type objectType) + { + return typeof(ProjectSnapshotHandle).IsAssignableFrom(objectType); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType != JsonToken.StartObject) + { + return null; + } + + var obj = JObject.Load(reader); + var filePath = obj[nameof(ProjectSnapshotHandle.FilePath)].Value(); + var configuration = obj[nameof(ProjectSnapshotHandle.Configuration)].ToObject(serializer); + + var id = obj[nameof(ProjectSnapshotHandle.WorkspaceProjectId)].Value(); + var workspaceProjectId = id == null ? null : ProjectId.CreateFromSerialized(Guid.Parse(id)); + + return new ProjectSnapshotHandle(filePath, configuration, workspaceProjectId); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var handle = (ProjectSnapshotHandle)value; + + writer.WriteStartObject(); + + writer.WritePropertyName(nameof(ProjectSnapshotHandle.FilePath)); + writer.WriteValue(handle.FilePath); + + if (handle.Configuration == null) + { + writer.WritePropertyName(nameof(ProjectSnapshotHandle.Configuration)); + writer.WriteNull(); + } + else + { + writer.WritePropertyName(nameof(ProjectSnapshotHandle.Configuration)); + serializer.Serialize(writer, handle.Configuration); + } + + if (handle.WorkspaceProjectId == null) + { + writer.WritePropertyName(nameof(ProjectSnapshotHandle.WorkspaceProjectId)); + writer.WriteNull(); + } + else + { + writer.WritePropertyName(nameof(ProjectSnapshotHandle.WorkspaceProjectId)); + writer.WriteValue(handle.WorkspaceProjectId.Id); + } + + writer.WriteEndObject(); + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/ProjectSnapshotJsonConverter.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/ProjectSnapshotJsonConverter.cs new file mode 100644 index 0000000000..988d42b44d --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/ProjectSnapshotJsonConverter.cs @@ -0,0 +1,40 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Newtonsoft.Json; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Serialization +{ + // We can't truly serialize a snapshot because it has access to a Workspace Project\ + // + // Instead we serialize to a ProjectSnapshotHandle and then use that to re-create the snapshot + // inside the remote host. + internal class ProjectSnapshotJsonConverter : JsonConverter + { + public static readonly ProjectSnapshotJsonConverter Instance = new ProjectSnapshotJsonConverter(); + + public override bool CanRead => false; + + public override bool CanWrite => true; + + public override bool CanConvert(Type objectType) + { + return typeof(ProjectSnapshot).IsAssignableFrom(objectType); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new NotSupportedException(); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var project = (ProjectSnapshot)value; + var handle = new ProjectSnapshotHandle(project.FilePath, project.Configuration, project.WorkspaceProject?.Id); + + ProjectSnapshotHandleJsonConverter.Instance.WriteJson(writer, handle, serializer); + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/RazorConfigurationJsonConverter.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/RazorConfigurationJsonConverter.cs new file mode 100644 index 0000000000..26be8c52b7 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/RazorConfigurationJsonConverter.cs @@ -0,0 +1,53 @@ +// 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; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Serialization +{ + internal class RazorConfigurationJsonConverter : JsonConverter + { + public static readonly RazorConfigurationJsonConverter Instance = new RazorConfigurationJsonConverter(); + + public override bool CanConvert(Type objectType) + { + return typeof(RazorConfiguration).IsAssignableFrom(objectType); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType != JsonToken.StartObject) + { + return null; + } + + var obj = JObject.Load(reader); + var configurationName = obj[nameof(RazorConfiguration.ConfigurationName)].Value(); + var languageVersion = obj[nameof(RazorConfiguration.LanguageVersion)].Value(); + var extensions = obj[nameof(RazorConfiguration.Extensions)].ToObject(serializer); + + return RazorConfiguration.Create(RazorLanguageVersion.Parse(languageVersion), configurationName, extensions); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var configuration = (RazorConfiguration)value; + + writer.WriteStartObject(); + + writer.WritePropertyName(nameof(RazorConfiguration.ConfigurationName)); + writer.WriteValue(configuration.ConfigurationName); + + writer.WritePropertyName(nameof(RazorConfiguration.LanguageVersion)); + writer.WriteValue(configuration.LanguageVersion.ToString()); + + writer.WritePropertyName(nameof(RazorConfiguration.Extensions)); + serializer.Serialize(writer, configuration.Extensions); + + writer.WriteEndObject(); + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorDiagnosticJsonConverter.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/RazorDiagnosticJsonConverter.cs similarity index 97% rename from src/Microsoft.VisualStudio.LanguageServices.Razor/RazorDiagnosticJsonConverter.cs rename to src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/RazorDiagnosticJsonConverter.cs index 85d0a4096b..003dbf9164 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorDiagnosticJsonConverter.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/RazorDiagnosticJsonConverter.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Razor.Language; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace Microsoft.VisualStudio.LanguageServices.Razor +namespace Microsoft.VisualStudio.LanguageServices.Razor.Serialization { internal class RazorDiagnosticJsonConverter : JsonConverter { diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/RazorExtensionJsonConverter.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/RazorExtensionJsonConverter.cs new file mode 100644 index 0000000000..0d84b8c939 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/RazorExtensionJsonConverter.cs @@ -0,0 +1,45 @@ +// 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; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Serialization +{ + internal class RazorExtensionJsonConverter : JsonConverter + { + public static readonly RazorExtensionJsonConverter Instance = new RazorExtensionJsonConverter(); + + public override bool CanConvert(Type objectType) + { + return typeof(RazorExtension).IsAssignableFrom(objectType); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType != JsonToken.StartObject) + { + return null; + } + + var obj = JObject.Load(reader); + var extensionName = obj[nameof(RazorExtension.ExtensionName)].Value(); + + return new SerializedRazorExtension(extensionName); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var extension = (RazorExtension)value; + + writer.WriteStartObject(); + + writer.WritePropertyName(nameof(RazorExtension.ExtensionName)); + writer.WriteValue(extension.ExtensionName); + + writer.WriteEndObject(); + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/SerializedRazorExtension.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/SerializedRazorExtension.cs new file mode 100644 index 0000000000..0f0292534f --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/SerializedRazorExtension.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; +using Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Serialization +{ + internal class SerializedRazorExtension : RazorExtension + { + public SerializedRazorExtension(string extensionName) + { + if (extensionName == null) + { + throw new ArgumentNullException(nameof(extensionName)); + } + + ExtensionName = extensionName; + } + + public override string ExtensionName { get; } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/TagHelperDescriptorJsonConverter.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/TagHelperDescriptorJsonConverter.cs similarity index 99% rename from src/Microsoft.VisualStudio.LanguageServices.Razor/TagHelperDescriptorJsonConverter.cs rename to src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/TagHelperDescriptorJsonConverter.cs index 406ea9a85f..271af9e1fd 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/TagHelperDescriptorJsonConverter.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/TagHelperDescriptorJsonConverter.cs @@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Razor.Language; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace Microsoft.VisualStudio.LanguageServices.Razor +namespace Microsoft.VisualStudio.LanguageServices.Razor.Serialization { internal class TagHelperDescriptorJsonConverter : JsonConverter { diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultProjectEngineFactoryServiceTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultProjectEngineFactoryServiceTest.cs index 12ecf74efe..1f418b2855 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultProjectEngineFactoryServiceTest.cs +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultProjectEngineFactoryServiceTest.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using System.Reflection; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor; @@ -31,28 +32,62 @@ namespace Microsoft.VisualStudio.Editor.Razor HostProject_For_1_0 = new HostProject("/TestPath/SomePath/Test.csproj", FallbackRazorConfiguration.MVC_1_0); HostProject_For_1_1 = new HostProject("/TestPath/SomePath/Test.csproj", FallbackRazorConfiguration.MVC_1_1); HostProject_For_2_0 = new HostProject("/TestPath/SomePath/Test.csproj", FallbackRazorConfiguration.MVC_2_0); + + HostProject_For_2_1 = new HostProject( + "/TestPath/SomePath/Test.csproj", + new ProjectSystemRazorConfiguration(RazorLanguageVersion.Version_2_1, "MVC-2.1", Array.Empty())); + HostProject_For_UnknownConfiguration = new HostProject( + "/TestPath/SomePath/Test.csproj", + new ProjectSystemRazorConfiguration(RazorLanguageVersion.Version_2_1, "Blazor-0.1", Array.Empty())); + + + CustomFactories = new Lazy[] + { + new Lazy( + () => new LegacyProjectEngineFactory_1_0(), + typeof(LegacyProjectEngineFactory_1_0).GetCustomAttribute()), + new Lazy( + () => new LegacyProjectEngineFactory_1_1(), + typeof(LegacyProjectEngineFactory_1_1).GetCustomAttribute()), + new Lazy( + () => new LegacyProjectEngineFactory_2_0(), + typeof(LegacyProjectEngineFactory_2_0).GetCustomAttribute()), + new Lazy( + () => new LegacyProjectEngineFactory_2_1(), + typeof(LegacyProjectEngineFactory_2_1).GetCustomAttribute()), + }; + + FallbackFactory = new FallbackProjectEngineFactory(); } + private Lazy[] CustomFactories { get; } + + private IFallbackProjectEngineFactory FallbackFactory { get; } + private HostProject HostProject_For_1_0 { get; } private HostProject HostProject_For_1_1 { get; } private HostProject HostProject_For_2_0 { get; } + private HostProject HostProject_For_2_1 { get; } + + private HostProject HostProject_For_UnknownConfiguration { get; } + // We don't actually look at the project, we rely on the ProjectStateManager private Project WorkspaceProject { get; } private Workspace Workspace { get; } [Fact] - public void Create_CreatesTemplateEngine_ForLatest() + public void Create_CreatesDesignTimeTemplateEngine_ForVersion2_1() { // Arrange var projectManager = new TestProjectSnapshotManager(Workspace); - projectManager.HostProjectAdded(HostProject_For_2_0); + projectManager.HostProjectAdded(HostProject_For_2_1); projectManager.WorkspaceProjectAdded(WorkspaceProject); - var factoryService = new DefaultProjectEngineFactoryService(projectManager); + var factoryService = new DefaultProjectEngineFactoryService(projectManager, FallbackFactory, CustomFactories); // Act var engine = factoryService.Create("/TestPath/SomePath/", b => @@ -62,6 +97,30 @@ namespace Microsoft.VisualStudio.Editor.Razor // Assert Assert.Single(engine.Engine.Features.OfType()); + Assert.Single(engine.Engine.Features.OfType()); + Assert.Single(engine.Engine.Features.OfType()); + Assert.Single(engine.Engine.Features.OfType()); + } + + [Fact] + public void Create_CreatesDesignTimeTemplateEngine_ForVersion2_0() + { + // Arrange + var projectManager = new TestProjectSnapshotManager(Workspace); + projectManager.HostProjectAdded(HostProject_For_2_0); + projectManager.WorkspaceProjectAdded(WorkspaceProject); + + var factoryService = new DefaultProjectEngineFactoryService(projectManager, FallbackFactory, CustomFactories); + + // Act + var engine = factoryService.Create("/TestPath/SomePath/", b => + { + b.Features.Add(new MyCoolNewFeature()); + }); + + // Assert + Assert.Single(engine.Engine.Features.OfType()); + Assert.Single(engine.Engine.Features.OfType()); Assert.Single(engine.Engine.Features.OfType()); Assert.Single(engine.Engine.Features.OfType()); } @@ -74,7 +133,7 @@ namespace Microsoft.VisualStudio.Editor.Razor projectManager.HostProjectAdded(HostProject_For_1_1); projectManager.WorkspaceProjectAdded(WorkspaceProject); - var factoryService = new DefaultProjectEngineFactoryService(projectManager); + var factoryService = new DefaultProjectEngineFactoryService(projectManager, FallbackFactory, CustomFactories); // Act var engine = factoryService.Create("/TestPath/SomePath/", b => @@ -84,6 +143,7 @@ namespace Microsoft.VisualStudio.Editor.Razor // Assert Assert.Single(engine.Engine.Features.OfType()); + Assert.Single(engine.Engine.Features.OfType()); Assert.Single(engine.Engine.Features.OfType()); Assert.Single(engine.Engine.Features.OfType()); } @@ -96,7 +156,7 @@ namespace Microsoft.VisualStudio.Editor.Razor projectManager.HostProjectAdded(HostProject_For_1_0); projectManager.WorkspaceProjectAdded(WorkspaceProject); - var factoryService = new DefaultProjectEngineFactoryService(projectManager); + var factoryService = new DefaultProjectEngineFactoryService(projectManager, FallbackFactory, CustomFactories); // Act var engine = factoryService.Create("/TestPath/SomePath/", b => @@ -106,17 +166,18 @@ namespace Microsoft.VisualStudio.Editor.Razor // Assert Assert.Single(engine.Engine.Features.OfType()); - Assert.Single(engine.Engine.Features.OfType()); - Assert.Empty(engine.Engine.Features.OfType()); + Assert.Empty(engine.Engine.Features.OfType()); + Assert.Empty(engine.Engine.Features.OfType()); + Assert.Empty(engine.Engine.Features.OfType()); } [Fact] - public void Create_UnknownProjectPath_UsesLatest() + public void Create_UnknownProject_UsesVersion2_0() { // Arrange var projectManager = new TestProjectSnapshotManager(Workspace); - var factoryService = new DefaultProjectEngineFactoryService(projectManager); + var factoryService = new DefaultProjectEngineFactoryService(projectManager, FallbackFactory, CustomFactories); // Act var engine = factoryService.Create("/TestPath/DifferentPath/", b => @@ -126,30 +187,33 @@ namespace Microsoft.VisualStudio.Editor.Razor // Assert Assert.Single(engine.Engine.Features.OfType()); + Assert.Single(engine.Engine.Features.OfType()); Assert.Single(engine.Engine.Features.OfType()); Assert.Single(engine.Engine.Features.OfType()); } [Fact] - public void Create_MvcReferenceNotFound_UsesLatest() + public void Create_ForUnknownConfiguration_UsesFallbackFactory() { // Arrange var projectManager = new TestProjectSnapshotManager(Workspace); - projectManager.HostProjectAdded(HostProject_For_2_0); + projectManager.HostProjectAdded(HostProject_For_UnknownConfiguration); projectManager.WorkspaceProjectAdded(WorkspaceProject); - var factoryService = new DefaultProjectEngineFactoryService(projectManager); + var factoryService = new DefaultProjectEngineFactoryService(projectManager, FallbackFactory, CustomFactories); // Act - var engine = factoryService.Create("/TestPath/DifferentPath/", b => + var engine = factoryService.Create("/TestPath/SomePath/", b => { b.Features.Add(new MyCoolNewFeature()); }); // Assert Assert.Single(engine.Engine.Features.OfType()); - Assert.Single(engine.Engine.Features.OfType()); - Assert.Single(engine.Engine.Features.OfType()); + Assert.Empty(engine.Engine.Features.OfType()); + Assert.Empty(engine.Engine.Features.OfType()); + Assert.Empty(engine.Engine.Features.OfType()); + Assert.Empty(engine.Engine.Features.OfType()); } private class MyCoolNewFeature : IRazorEngineFeature diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/Microsoft.VisualStudio.Editor.Razor.Test.csproj b/test/Microsoft.VisualStudio.Editor.Razor.Test/Microsoft.VisualStudio.Editor.Razor.Test.csproj index 6f4876aec0..e40eebd7e7 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/Microsoft.VisualStudio.Editor.Razor.Test.csproj +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/Microsoft.VisualStudio.Editor.Razor.Test.csproj @@ -10,6 +10,8 @@ + + diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj index 09ab7e7662..a4b7553241 100644 --- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj @@ -18,6 +18,8 @@ + + diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/OOPTagHelperResolverTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/OOPTagHelperResolverTest.cs new file mode 100644 index 0000000000..cb9a4e8a6b --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/OOPTagHelperResolverTest.cs @@ -0,0 +1,177 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.VisualStudio.Editor.Razor; +using Moq; +using Xunit; + +namespace Microsoft.VisualStudio.LanguageServices.Razor +{ + public class OOPTagHelperResolverTest + { + public OOPTagHelperResolverTest() + { + HostProject_For_2_0 = new HostProject("Test.csproj", FallbackRazorConfiguration.MVC_2_0); + HostProject_For_NonSerializableConfiguration = new HostProject( + "Test.csproj", + new ProjectSystemRazorConfiguration(RazorLanguageVersion.Version_2_1, "Blazor-0.1", Array.Empty())); + + CustomFactories = new Lazy[] + { + new Lazy( + () => new LegacyProjectEngineFactory_2_0(), + typeof(LegacyProjectEngineFactory_2_0).GetCustomAttribute()), + + // We don't really use this factory, we just use it to ensure that the call is going to go out of process. + new Lazy( + () => new LegacyProjectEngineFactory_2_1(), + new ExportCustomProjectEngineFactoryAttribute("Blazor-0.1") { SupportsSerialization = false, }), + }; + + FallbackFactory = new FallbackProjectEngineFactory(); + + Workspace = new AdhocWorkspace(); + + var info = ProjectInfo.Create(ProjectId.CreateNewId("Test"), VersionStamp.Default, "Test", "Test", LanguageNames.CSharp, filePath: "Test.csproj"); + WorkspaceProject = Workspace.CurrentSolution.AddProject(info).GetProject(info.Id); + + ErrorReporter = new DefaultErrorReporter(); + ProjectManager = new TestProjectSnapshotManager(Workspace); + EngineFactory = new DefaultProjectEngineFactoryService(ProjectManager, FallbackFactory, CustomFactories); + } + + private ErrorReporter ErrorReporter { get; } + + private RazorProjectEngineFactoryService EngineFactory { get; } + + private Lazy[] CustomFactories { get; } + + private IFallbackProjectEngineFactory FallbackFactory { get; } + + private HostProject HostProject_For_2_0 { get; } + + private HostProject HostProject_For_NonSerializableConfiguration { get; } + + private ProjectSnapshotManagerBase ProjectManager { get; } + + private Project WorkspaceProject { get; } + + private Workspace Workspace { get; } + + [Fact] + public async Task GetTagHelpersAsync_WithNonInitializedProject_Noops() + { + // Arrange + ProjectManager.HostProjectAdded(HostProject_For_2_0); + + var project = ProjectManager.GetProjectWithFilePath("Test.csproj"); + + var resolver = new TestTagHelperResolver(EngineFactory, ErrorReporter, Workspace); + + var result = await resolver.GetTagHelpersAsync(project); + + // Assert + Assert.Same(TagHelperResolutionResult.Empty, result); + } + + [Fact] + public async Task GetTagHelpersAsync_WithSerializableCustomFactory_GoesOutOfProcess() + { + // Arrange + ProjectManager.HostProjectAdded(HostProject_For_2_0); + ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + + var project = ProjectManager.GetProjectWithFilePath("Test.csproj"); + + var resolver = new TestTagHelperResolver(EngineFactory, ErrorReporter, Workspace) + { + OnResolveOutOfProcess = (f, p) => + { + Assert.Same(CustomFactories[0].Value, f); + Assert.Same(project, p); + + return Task.FromResult(TagHelperResolutionResult.Empty); + }, + }; + + var result = await resolver.GetTagHelpersAsync(project); + + // Assert + Assert.Same(TagHelperResolutionResult.Empty, result); + } + + [Fact] + public async Task GetTagHelpersAsync_WithNonSerializableCustomFactory_StaysInProcess() + { + // Arrange + ProjectManager.HostProjectAdded(HostProject_For_NonSerializableConfiguration); + ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + + var project = ProjectManager.GetProjectWithFilePath("Test.csproj"); + + var resolver = new TestTagHelperResolver(EngineFactory, ErrorReporter, Workspace) + { + OnResolveInProcess = (p) => + { + Assert.Same(project, p); + + return Task.FromResult(TagHelperResolutionResult.Empty); + }, + }; + + var result = await resolver.GetTagHelpersAsync(project); + + // Assert + Assert.Same(TagHelperResolutionResult.Empty, result); + + } + + private class TestTagHelperResolver : OOPTagHelperResolver + { + public TestTagHelperResolver(RazorProjectEngineFactoryService engineFactory, ErrorReporter errorReporter, Workspace workspace) + : base(engineFactory, errorReporter, workspace) + { + } + + public Func> OnResolveOutOfProcess { get; set; } + + public Func> OnResolveInProcess { get; set; } + + protected override Task ResolveTagHelpersOutOfProcessAsync(IProjectEngineFactory factory, ProjectSnapshot project) + { + Assert.NotNull(OnResolveOutOfProcess); + return OnResolveOutOfProcess(factory, project); + } + + protected override Task ResolveTagHelpersInProcessAsync(ProjectSnapshot project) + { + Assert.NotNull(OnResolveInProcess); + return OnResolveInProcess(project); + } + } + private class TestProjectSnapshotManager : DefaultProjectSnapshotManager + { + public TestProjectSnapshotManager(Workspace workspace) + : base( + Mock.Of(), + Mock.Of(), + Mock.Of(), + Enumerable.Empty(), + workspace) + { + } + + protected override void NotifyBackgroundWorker(ProjectSnapshotUpdateContext context) + { + } + } + } +} diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Serialization/ProjectSnapshotHandleSerializationTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Serialization/ProjectSnapshotHandleSerializationTest.cs new file mode 100644 index 0000000000..347c533f8a --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Serialization/ProjectSnapshotHandleSerializationTest.cs @@ -0,0 +1,73 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Serialization +{ + public class ProjectSnapshotHandleSerializationTest + { + public ProjectSnapshotHandleSerializationTest() + { + var converters = new JsonConverterCollection(); + converters.RegisterRazorConverters(); + Converters = converters.ToArray(); + } + + public JsonConverter[] Converters { get; } + + [Fact] + public void ProjectSnapshotHandleJsonConverter_Serialization_CanKindaRoundTrip() + { + // Arrange + var snapshot = new ProjectSnapshotHandle( + "Test.csproj", + new ProjectSystemRazorConfiguration( + RazorLanguageVersion.Version_1_1, + "Test", + new[] + { + new ProjectSystemRazorExtension("Test-Extension1"), + new ProjectSystemRazorExtension("Test-Extension2"), + }), + ProjectId.CreateFromSerialized(Guid.NewGuid(), "Test")); + + // Act + var json = JsonConvert.SerializeObject(snapshot, Converters); + var obj = JsonConvert.DeserializeObject(json, Converters); + + // Assert + Assert.Equal(snapshot.FilePath, obj.FilePath); + Assert.Equal(snapshot.Configuration.ConfigurationName, obj.Configuration.ConfigurationName); + Assert.Collection( + snapshot.Configuration.Extensions, + e => Assert.Equal("Test-Extension1", e.ExtensionName), + e => Assert.Equal("Test-Extension2", e.ExtensionName)); + Assert.Equal(snapshot.Configuration.LanguageVersion, obj.Configuration.LanguageVersion); + Assert.Equal(snapshot.WorkspaceProjectId.Id, obj.WorkspaceProjectId.Id); + } + + [Fact] + public void ProjectSnapshotHandleJsonConverter_SerializationWithNulls_CanKindaRoundTrip() + { + // Arrange + var snapshot = new ProjectSnapshotHandle("Test.csproj", null, null); + + // Act + var json = JsonConvert.SerializeObject(snapshot, Converters); + var obj = JsonConvert.DeserializeObject(json, Converters); + + // Assert + Assert.Equal(snapshot.FilePath, obj.FilePath); + Assert.Null(obj.Configuration); + Assert.Null(obj.WorkspaceProjectId); + } + } +} diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Serialization/RazorConfigurationSerializationTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Serialization/RazorConfigurationSerializationTest.cs new file mode 100644 index 0000000000..2b084a8531 --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Serialization/RazorConfigurationSerializationTest.cs @@ -0,0 +1,50 @@ +// 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.Linq; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Serialization +{ + public class RazorConfigurationSerializationTest + { + public RazorConfigurationSerializationTest() + { + var converters = new JsonConverterCollection(); + converters.RegisterRazorConverters(); + Converters = converters.ToArray(); + } + + public JsonConverter[] Converters { get; } + + [Fact] + public void RazorConfigurationJsonConverter_Serialization_CanRoundTrip() + { + // Arrange + var configuration = new ProjectSystemRazorConfiguration( + RazorLanguageVersion.Version_1_1, + "Test", + new[] + { + new ProjectSystemRazorExtension("Test-Extension1"), + new ProjectSystemRazorExtension("Test-Extension2"), + }); + + // Act + var json = JsonConvert.SerializeObject(configuration, Converters); + var obj = JsonConvert.DeserializeObject(json, Converters); + + // Assert + Assert.Equal(configuration.ConfigurationName, obj.ConfigurationName); + Assert.Collection( + configuration.Extensions, + e => Assert.Equal("Test-Extension1", e.ExtensionName), + e => Assert.Equal("Test-Extension2", e.ExtensionName)); + Assert.Equal(configuration.LanguageVersion, obj.LanguageVersion); + } + } +} diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Serialization/RazorExtensionSerializationTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Serialization/RazorExtensionSerializationTest.cs new file mode 100644 index 0000000000..0f93fb5bd7 --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Serialization/RazorExtensionSerializationTest.cs @@ -0,0 +1,38 @@ +// 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.Linq; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Serialization +{ + public class RazorExtensionSerializationTest + { + public RazorExtensionSerializationTest() + { + var converters = new JsonConverterCollection(); + converters.RegisterRazorConverters(); + Converters = converters.ToArray(); + } + + public JsonConverter[] Converters { get; } + + [Fact] + public void RazorExensionJsonConverter_Serialization_CanRoundTrip() + { + // Arrange + var extension = new ProjectSystemRazorExtension("Test"); + + // Act + var json = JsonConvert.SerializeObject(extension, Converters); + var obj = JsonConvert.DeserializeObject(json, Converters); + + // Assert + Assert.Equal(extension.ExtensionName, obj.ExtensionName); + } + } +} diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/TagHelperDescriptorSerializationTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Serialization/TagHelperDescriptorSerializationTest.cs similarity index 99% rename from test/Microsoft.VisualStudio.LanguageServices.Razor.Test/TagHelperDescriptorSerializationTest.cs rename to test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Serialization/TagHelperDescriptorSerializationTest.cs index 4e37727804..bf799a094a 100644 --- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/TagHelperDescriptorSerializationTest.cs +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Serialization/TagHelperDescriptorSerializationTest.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Legacy; +using Microsoft.VisualStudio.LanguageServices.Razor.Serialization; using Newtonsoft.Json; using Xunit; diff --git a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/RazorInfoViewModel.cs b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/RazorInfoViewModel.cs index 7efaa43199..82054dbabf 100644 --- a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/RazorInfoViewModel.cs +++ b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/RazorInfoViewModel.cs @@ -177,7 +177,8 @@ namespace Microsoft.VisualStudio.RazorExtension.RazorInfo .Select(reference => reference.Display) .Select(filter => Path.GetFileNameWithoutExtension(filter)); var projectFilters = project.AllProjectReferences.Select(filter => solution.GetProject(filter.ProjectId).AssemblyName); - var resolutionResult = await _tagHelperResolver.GetTagHelpersAsync(project, CancellationToken.None); + + var resolutionResult = await _tagHelperResolver.GetTagHelpersAsync(projectViewModel.Snapshot.Project); var files = GetCshtmlDocuments(project);