diff --git a/eng/targets/Services.props b/eng/targets/Services.props index 800d927381f..2103a0cd76a 100644 --- a/eng/targets/Services.props +++ b/eng/targets/Services.props @@ -19,6 +19,7 @@ + diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteMEFInitializationService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteMEFInitializationService.cs new file mode 100644 index 00000000000..0a457942253 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteMEFInitializationService.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CodeAnalysis.Razor.Remote; + +internal interface IRemoteMEFInitializationService : IRemoteJsonService +{ + ValueTask InitializeAsync(string cacheDirectory, CancellationToken cancellationToken); +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs index 8e107be777d..8f6fab6b8a2 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs @@ -43,6 +43,7 @@ internal static class RazorServices (typeof(IRemoteCompletionService), null), (typeof(IRemoteCodeActionsService), null), (typeof(IRemoteFindAllReferencesService), null), + (typeof(IRemoteMEFInitializationService), null), ]; private const string ComponentName = "Razor"; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Initialization/RemoteMEFInitializationService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Initialization/RemoteMEFInitializationService.cs new file mode 100644 index 00000000000..a81d4cb6a8f --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Initialization/RemoteMEFInitializationService.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor; +using Microsoft.CodeAnalysis.ExternalAccess.Razor.Api; +using Microsoft.CodeAnalysis.Razor.Remote; +using Microsoft.ServiceHub.Framework; +using static Microsoft.CodeAnalysis.Remote.Razor.RazorBrokeredServiceBase; + +namespace Microsoft.CodeAnalysis.Remote.Razor; + +/// +/// A special service that is used to initialize the MEF composition for Razor in the remote host. +/// +/// +/// It's special because it doesn't use MEF. Nor can it use anything else really. +/// +internal sealed class RemoteMEFInitializationService : IRemoteMEFInitializationService +{ + internal sealed class Factory : FactoryBase + { + protected override Task CreateInternalAsync(Stream? stream, IServiceProvider hostProvidedServices, IServiceBroker? serviceBroker) + { + var traceSource = (TraceSource?)hostProvidedServices.GetService(typeof(TraceSource)); + + var service = new RemoteMEFInitializationService(); + if (stream is not null) + { + var serverConnection = CreateServerConnection(stream, traceSource); + ConnectService(serverConnection, service); + } + + return Task.FromResult(service); + } + + protected override IRemoteMEFInitializationService CreateService(in ServiceArgs args) + => Assumed.Unreachable("This service overrides CreateInternalAsync to avoid MEF instatiation, so the CreateService method should never be called."); + } + + public ValueTask InitializeAsync(string cacheDirectory, CancellationToken cancellationToken) + { + return RazorBrokeredServiceImplementation.RunServiceAsync(_ => + { + RemoteMefComposition.CacheDirectory = cacheDirectory; + return new(); + }, cancellationToken); + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorBrokeredServiceBase.FactoryBase`1.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorBrokeredServiceBase.FactoryBase`1.cs index aa672fbdb78..0416679d0a2 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorBrokeredServiceBase.FactoryBase`1.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorBrokeredServiceBase.FactoryBase`1.cs @@ -60,7 +60,7 @@ private Task CreateAsync(Stream stream, IServiceProvider hostProvidedSer #endif } - protected async Task CreateInternalAsync( + protected virtual async Task CreateInternalAsync( Stream? stream, IServiceProvider hostProvidedServices, IServiceBroker? serviceBroker) @@ -103,21 +103,31 @@ protected async Task CreateInternalAsync( return CreateService(in inProcArgs); } + var serverConnection = CreateServerConnection(stream, traceSource); + + var args = new ServiceArgs(serviceBroker.AssumeNotNull(), exportProvider, targetLoggerFactory, workspaceProvider, serverConnection, brokeredServiceData?.Interceptor); + var service = CreateService(in args); + + ConnectService(serverConnection, service); + + return service; + } + + protected static ServiceRpcDescriptor.RpcConnection CreateServerConnection(Stream stream, TraceSource? traceSource) + { var pipe = stream.UsePipe(); var descriptor = typeof(IRemoteJsonService).IsAssignableFrom(typeof(TService)) ? RazorServices.JsonDescriptors.GetDescriptorForServiceFactory(typeof(TService)) : RazorServices.Descriptors.GetDescriptorForServiceFactory(typeof(TService)); var serverConnection = descriptor.WithTraceSource(traceSource).ConstructRpcConnection(pipe); + return serverConnection; + } - var args = new ServiceArgs(serviceBroker.AssumeNotNull(), exportProvider, targetLoggerFactory, workspaceProvider, serverConnection, brokeredServiceData?.Interceptor); - - var service = CreateService(in args); - + protected static void ConnectService(ServiceRpcDescriptor.RpcConnection serverConnection, TService service) + { serverConnection.AddLocalRpcTarget(service); serverConnection.StartListening(); - - return service; } } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RemoteMefComposition.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RemoteMefComposition.cs index a054ef38c7e..51d22e5a7c2 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RemoteMefComposition.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RemoteMefComposition.cs @@ -1,10 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Reflection; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor; +using Microsoft.AspNetCore.Razor.Utilities; using Microsoft.VisualStudio.Composition; using Microsoft.VisualStudio.Threading; @@ -21,9 +26,13 @@ internal sealed class RemoteMefComposition joinableTaskFactory: null); private static readonly AsyncLazy s_lazyExportProvider = new( - static () => CreateExportProviderAsync(CancellationToken.None), + static () => CreateExportProviderAsync(CacheDirectory, CancellationToken.None), joinableTaskFactory: null); + private static Task? s_saveCacheFileTask; + + public static string? CacheDirectory { get; set; } + /// /// Gets a built from . Note that the /// same instance is returned for subsequent calls to this method. @@ -52,17 +61,125 @@ private static async Task CreateConfigurationAsync(Can /// Creates a new MEF composition and returns an . The catalog and configuration /// are reused for subsequent calls to this method. /// - public static async Task CreateExportProviderAsync(CancellationToken cancellationToken) + public static async Task CreateExportProviderAsync(string? cacheDirectory, CancellationToken cancellationToken) { + var cache = new CachedComposition(); + var compositionCacheFile = GetCompositionCacheFile(cacheDirectory); + if (await TryLoadCachedExportProviderAsync(cache, compositionCacheFile, cancellationToken).ConfigureAwait(false) is { } cachedProvider) + { + return cachedProvider; + } + var configuration = await s_lazyConfiguration.GetValueAsync(cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); var runtimeComposition = RuntimeComposition.CreateRuntimeComposition(configuration); var exportProviderFactory = runtimeComposition.CreateExportProviderFactory(); + // We don't need to block on saving the cache, because if it fails or is corrupt, we'll just try again next time, but + // we capture the task just so that tests can verify things. + s_saveCacheFileTask = TrySaveCachedExportProviderAsync(cache, compositionCacheFile, runtimeComposition, cancellationToken); + return exportProviderFactory.CreateExportProvider(); } + private static async Task TryLoadCachedExportProviderAsync(CachedComposition cache, string? compositionCacheFile, CancellationToken cancellationToken) + { + if (compositionCacheFile is null) + { + return null; + } + + try + { + if (File.Exists(compositionCacheFile)) + { + var resolver = new Resolver(SimpleAssemblyLoader.Instance); + using var cacheStream = new FileStream(compositionCacheFile, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 4096, useAsync: true); + var cachedFactory = await cache.LoadExportProviderFactoryAsync(cacheStream, resolver, cancellationToken).ConfigureAwait(false); + return cachedFactory.CreateExportProvider(); + } + } + catch (Exception) + { + // We ignore all errors when loading the cache, because if the cache is corrupt we will just create a new export provider. + } + + return null; + } + + private static async Task TrySaveCachedExportProviderAsync(CachedComposition cache, string? compositionCacheFile, RuntimeComposition runtimeComposition, CancellationToken cancellationToken) + { + if (compositionCacheFile is null) + { + return; + } + + try + { + var cacheDirectory = Path.GetDirectoryName(compositionCacheFile).AssumeNotNull(); + var directoryInfo = Directory.CreateDirectory(cacheDirectory); + + CleanCacheDirectory(directoryInfo, cancellationToken); + + var tempFilePath = Path.Combine(cacheDirectory, Path.GetRandomFileName()); + using (var cacheStream = new FileStream(compositionCacheFile, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true)) + { + await cache.SaveAsync(runtimeComposition, cacheStream, cancellationToken).ConfigureAwait(false); + } + + File.Move(tempFilePath, compositionCacheFile); + } + catch (Exception) + { + // We ignore all errors when saving the cache, because if something goes wrong, the next run will just create a new export provider. + } + } + + private static void CleanCacheDirectory(DirectoryInfo directoryInfo, CancellationToken cancellationToken) + { + try + { + // Delete any existing cached files. + foreach (var fileInfo in directoryInfo.EnumerateFiles()) + { + // Failing to delete any file is fine, we'll just try again the next VS session in which we attempt + // to write a new cache + fileInfo.Delete(); + cancellationToken.ThrowIfCancellationRequested(); + } + } + catch (Exception) + { + // We ignore all errors when cleaning the cache directory, because we'll try again if the cache is corrupt. + } + } + + [return: NotNullIfNotNull(nameof(cacheDirectory))] + private static string? GetCompositionCacheFile(string? cacheDirectory) + { + if (cacheDirectory is null) + { + return null; + } + + var checksum = new Checksum.Builder(); + foreach (var assembly in Assemblies) + { + var assemblyPath = assembly.Location.AssumeNotNull(); + checksum.AppendData(Path.GetFileName(assemblyPath)); + checksum.AppendData(File.GetLastWriteTimeUtc(assemblyPath).ToString("F")); + } + + // Create base64 string of the hash. + var hashAsBase64String = checksum.FreeAndGetChecksum().ToBase64String(); + + // Convert to filename safe base64 string. + hashAsBase64String = hashAsBase64String.Replace('+', '-').Replace('/', '_').TrimEnd('='); + + return Path.Combine(cacheDirectory, $"razor.mef.{hashAsBase64String}.cache"); + } + private sealed class SimpleAssemblyLoader : IAssemblyLoader { public static readonly IAssemblyLoader Instance = new SimpleAssemblyLoader(); @@ -83,4 +200,19 @@ public Assembly LoadAssembly(string assemblyFullName, string? codeBasePath) return LoadAssembly(assemblyName); } } + + public static class TestAccessor + { + public static Task? SaveCacheFileTask => s_saveCacheFileTask; + + public static void ClearSaveCacheFileTask() + { + s_saveCacheFileTask = null; + } + + public static string GetCacheCompositionFile(string cacheDirectory) + { + return GetCompositionCacheFile(cacheDirectory); + } + } } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Remote/RemoteServiceInvoker.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Remote/RemoteServiceInvoker.cs index cb0fce18ca8..6e4b6469085 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Remote/RemoteServiceInvoker.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Remote/RemoteServiceInvoker.cs @@ -16,6 +16,9 @@ using Microsoft.CodeAnalysis.Razor.SemanticTokens; using Microsoft.CodeAnalysis.Razor.Telemetry; using Microsoft.CodeAnalysis.Razor.Workspaces; +using Microsoft.VisualStudio.Settings; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Settings; namespace Microsoft.VisualStudio.Razor.Remote; @@ -26,12 +29,14 @@ internal sealed class RemoteServiceInvoker( LanguageServerFeatureOptions languageServerFeatureOptions, IClientCapabilitiesService clientCapabilitiesService, ISemanticTokensLegendService semanticTokensLegendService, + SVsServiceProvider serviceProvider, ITelemetryReporter telemetryReporter, ILoggerFactory loggerFactory) : IRemoteServiceInvoker, IDisposable { private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions; private readonly IClientCapabilitiesService _clientCapabilitiesService = clientCapabilitiesService; private readonly ISemanticTokensLegendService _semanticTokensLegendService = semanticTokensLegendService; + private readonly IServiceProvider _serviceProvider = serviceProvider; private readonly ITelemetryReporter _telemetryReporter = telemetryReporter; private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); @@ -163,8 +168,15 @@ async Task InitializeCoreAsync(bool oopInitialized, bool lspInitialized) await _initializeLspTask.ConfigureAwait(false); } - Task InitializeOOPAsync(RazorRemoteHostClient remoteClient) + async Task InitializeOOPAsync(RazorRemoteHostClient remoteClient) { + // The first call to OOP must be to initialize the MEF services, because everything after that relies on MEF. + var localSettingsDirectory = new ShellSettingsManager(_serviceProvider).GetApplicationDataFolder(ApplicationDataFolder.LocalSettings); + var cacheDirectory = Path.Combine(localSettingsDirectory, "Razor", "RemoteMEFCache"); + await remoteClient.TryInvokeAsync( + (s, ct) => s.InitializeAsync(cacheDirectory, ct), + _disposeTokenSource.Token).ConfigureAwait(false); + var initParams = new RemoteClientInitializationOptions { UseRazorCohostServer = _languageServerFeatureOptions.UseRazorCohostServer, @@ -179,11 +191,10 @@ Task InitializeOOPAsync(RazorRemoteHostClient remoteClient) _logger.LogDebug($"First OOP call, so initializing OOP service."); - return remoteClient + await remoteClient .TryInvokeAsync( (s, ct) => s.InitializeAsync(initParams, ct), - _disposeTokenSource.Token) - .AsTask(); + _disposeTokenSource.Token).ConfigureAwait(false); } Task InitializeLspAsync(RazorRemoteHostClient remoteClient) diff --git a/src/Razor/src/Microsoft.VisualStudioCode.RazorExtension/Services/VSCodeRemoteServicesInitializer.cs b/src/Razor/src/Microsoft.VisualStudioCode.RazorExtension/Services/VSCodeRemoteServicesInitializer.cs index b5d47c06a99..9d4ef8bafdc 100644 --- a/src/Razor/src/Microsoft.VisualStudioCode.RazorExtension/Services/VSCodeRemoteServicesInitializer.cs +++ b/src/Razor/src/Microsoft.VisualStudioCode.RazorExtension/Services/VSCodeRemoteServicesInitializer.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Composition; +using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; @@ -9,6 +10,7 @@ using Microsoft.CodeAnalysis.Razor.Remote; using Microsoft.CodeAnalysis.Razor.SemanticTokens; using Microsoft.CodeAnalysis.Razor.Workspaces; +using Microsoft.CodeAnalysis.Remote.Razor; using Microsoft.VisualStudio.Razor.LanguageClient.Cohost; namespace Microsoft.VisualStudioCode.RazorExtension.Services; @@ -41,6 +43,9 @@ public async Task StartupAsync(VSInternalClientCapabilities clientCapabilities, // we know this is VS Code specific, its all just smoke and mirrors anyway. We can avoid the smoke :) var serviceInterceptor = new VSCodeBrokeredServiceInterceptor(); + // First things first, set the cache directory for the MEF composition. + RemoteMefComposition.CacheDirectory = Path.Combine(Path.GetDirectoryName(this.GetType().Assembly.Location)!, "cache"); + var logger = _loggerFactory.GetOrCreateLogger(); logger.LogDebug("Initializing remote services."); var service = await InProcServiceFactory.CreateServiceAsync(serviceInterceptor, _workspaceProvider, _loggerFactory).ConfigureAwait(false); diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Remote.Razor.Test/RazorServicesTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Remote.Razor.Test/RazorServicesTest.cs index f58758ee278..f8ce7b09803 100644 --- a/src/Razor/test/Microsoft.CodeAnalysis.Remote.Razor.Test/RazorServicesTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Remote.Razor.Test/RazorServicesTest.cs @@ -54,7 +54,6 @@ public void JsonServicesHaveTheRightParameters(Type serviceType, Type? _) { Assert.Fail($"Method {method.Name} in a Json service has a pinned solution info wrapper parameter that isn't Json serializable"); } - } } } diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Remote.Razor.Test/RemoteMefCompositionTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Remote.Razor.Test/RemoteMefCompositionTest.cs new file mode 100644 index 00000000000..84f4ee61c8a --- /dev/null +++ b/src/Razor/test/Microsoft.CodeAnalysis.Remote.Razor.Test/RemoteMefCompositionTest.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.CodeAnalysis.Remote.Razor; +using Microsoft.CodeAnalysis.Test.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.CodeAnalysis.Razor.Remote; + +public class RemoteMefCompositionTest(ITestOutputHelper testOutputHelper) : ToolingTestBase(testOutputHelper) +{ + [Fact] + public async Task CompositionIsCached() + { + using var tempRoot = new TempRoot(); + var cacheDirectory = tempRoot.CreateDirectory().Path; + var exportProvider = await RemoteMefComposition.CreateExportProviderAsync(cacheDirectory, DisposalToken); + + Assert.NotNull(RemoteMefComposition.TestAccessor.SaveCacheFileTask); + await RemoteMefComposition.TestAccessor.SaveCacheFileTask; + + Assert.Single(Directory.GetFiles(cacheDirectory)); + } + + [Fact] + public async Task CacheFileIsUsed() + { + using var tempRoot = new TempRoot(); + var cacheDirectory = tempRoot.CreateDirectory().Path; + var exportProvider = await RemoteMefComposition.CreateExportProviderAsync(cacheDirectory, DisposalToken); + + Assert.NotNull(RemoteMefComposition.TestAccessor.SaveCacheFileTask); + await RemoteMefComposition.TestAccessor.SaveCacheFileTask; + + Assert.Single(Directory.GetFiles(cacheDirectory)); + + RemoteMefComposition.TestAccessor.ClearSaveCacheFileTask(); + + exportProvider = await RemoteMefComposition.CreateExportProviderAsync(cacheDirectory, DisposalToken); + + Assert.Null(RemoteMefComposition.TestAccessor.SaveCacheFileTask); + } + + [Fact] + public async Task CorruptCacheFileIsOverwritten() + { + using var tempRoot = new TempRoot(); + var cacheDirectory = tempRoot.CreateDirectory().Path; + var cacheFile = RemoteMefComposition.TestAccessor.GetCacheCompositionFile(cacheDirectory); + + File.WriteAllText(cacheFile, "This is not a valid cache file."); + + var exportProvider = await RemoteMefComposition.CreateExportProviderAsync(cacheDirectory, DisposalToken); + + Assert.NotNull(RemoteMefComposition.TestAccessor.SaveCacheFileTask); + await RemoteMefComposition.TestAccessor.SaveCacheFileTask; + + Assert.Single(Directory.GetFiles(cacheDirectory)); + Assert.True(new FileInfo(cacheFile).Length > 35); + } + + [Fact] + public async Task CleansOldCacheFiles() + { + using var tempRoot = new TempRoot(); + var cacheDirectory = tempRoot.CreateDirectory().Path; + Directory.CreateDirectory(cacheDirectory); + + File.WriteAllText(Path.Combine(cacheDirectory, Path.GetRandomFileName()), ""); + File.WriteAllText(Path.Combine(cacheDirectory, Path.GetRandomFileName()), ""); + File.WriteAllText(Path.Combine(cacheDirectory, Path.GetRandomFileName()), ""); + File.WriteAllText(Path.Combine(cacheDirectory, Path.GetRandomFileName()), ""); + + Assert.Equal(4, Directory.GetFiles(cacheDirectory).Length); + + var cacheFile = RemoteMefComposition.TestAccessor.GetCacheCompositionFile(cacheDirectory); + + var exportProvider = await RemoteMefComposition.CreateExportProviderAsync(cacheDirectory, DisposalToken); + + Assert.NotNull(RemoteMefComposition.TestAccessor.SaveCacheFileTask); + await RemoteMefComposition.TestAccessor.SaveCacheFileTask; + + Assert.Single(Directory.GetFiles(cacheDirectory)); + Assert.True(new FileInfo(cacheFile).Length > 35); + } +} diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTest.cs index 769b29558e9..99d040a0a34 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTest.cs @@ -61,7 +61,8 @@ public void RegistrationsProvideFilter() .AddExcludedPartTypes(typeof(ILspEditorFeatureDetector)) .AddParts(typeof(TestLspEditorFeatureDetector)) .AddExcludedPartTypes(typeof(IIncompatibleProjectService)) - .AddParts(typeof(TestIncompatibleProjectService)); + .AddParts(typeof(TestIncompatibleProjectService)) + .AddParts(typeof(TestVsServiceProvider)); using var exportProvider = testComposition.ExportProviderFactory.CreateExportProvider(); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs index 2b5f1cbaed5..1cbded648c1 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs @@ -59,7 +59,7 @@ protected override async Task InitializeAsync() // Note that this uses a cached catalog and configuration for performance. try { - _exportProvider = await RemoteMefComposition.CreateExportProviderAsync(DisposalToken); + _exportProvider = await RemoteMefComposition.CreateExportProviderAsync(cacheDirectory: null, DisposalToken); } catch (CompositionFailedException ex) when (ex.Errors is not null) { diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/TestVsServiceProvider.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/TestVsServiceProvider.cs new file mode 100644 index 00000000000..b844f913015 --- /dev/null +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/TestVsServiceProvider.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Composition; +using Microsoft.AspNetCore.Razor; +using Microsoft.VisualStudio.Shell; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; + +[Shared] +[PartNotDiscoverable] +[Export(typeof(SVsServiceProvider))] +internal class TestVsServiceProvider : SVsServiceProvider +{ + public object GetService(Type serviceType) + { + return Assumed.Unreachable(); + } +}