diff --git a/Roslyn.sln b/Roslyn.sln
index 704cad8e79ba5..0d508cfec2548 100644
--- a/Roslyn.sln
+++ b/Roslyn.sln
@@ -577,11 +577,11 @@ Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Microsoft.CodeAnalysis.Cont
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CodeAnalysis.Contracts.Package", "src\Dependencies\Contracts\Microsoft.CodeAnalysis.Contracts.Package.csproj", "{A8D5CFFA-7F9E-C35B-4F19-D63F6EC1D5CA}"
EndProject
-Project("{9a19103f-16f7-4668-be54-9a1e7a4f7556}") = "Microsoft.CodeAnalysis.ExternalAccess.Razor", "src\Tools\ExternalAccess\Razor\EditorFeatures\Microsoft.CodeAnalysis.ExternalAccess.Razor.csproj", "{068CD9AA-CEC3-CA68-1BAB-2B1B9FD711D3}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CodeAnalysis.ExternalAccess.Razor", "src\Tools\ExternalAccess\Razor\EditorFeatures\Microsoft.CodeAnalysis.ExternalAccess.Razor.csproj", "{068CD9AA-CEC3-CA68-1BAB-2B1B9FD711D3}"
EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Microsoft.CodeAnalysis.ExternalAccess.Razor.Shared", "src\Tools\ExternalAccess\Razor\Shared\Microsoft.CodeAnalysis.ExternalAccess.Razor.Shared.shproj", "{4853A78A-4EC4-4D86-9F02-D0DDEAE85520}"
EndProject
-Project("{9a19103f-16f7-4668-be54-9a1e7a4f7556}") = "Microsoft.CodeAnalysis.ExternalAccess.Razor.Features", "src\Tools\ExternalAccess\Razor\Features\Microsoft.CodeAnalysis.ExternalAccess.Razor.Features.csproj", "{D5A8E20C-E8D2-4A57-906A-263994D8731D}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CodeAnalysis.ExternalAccess.Razor.Features", "src\Tools\ExternalAccess\Razor\Features\Microsoft.CodeAnalysis.ExternalAccess.Razor.Features.csproj", "{D5A8E20C-E8D2-4A57-906A-263994D8731D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.LanguageServices.ExternalAccess.Copilot", "src\VisualStudio\ExternalAccess\Copilot\Microsoft.VisualStudio.LanguageServices.ExternalAccess.Copilot.csproj", "{9EB058F3-10C9-8F3F-AD9E-72CB362A0928}"
EndProject
@@ -729,6 +729,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenerateDocumentationAndCon
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestReferenceAssembly", "src\RoslynAnalyzers\TestReferenceAssembly\TestReferenceAssembly.csproj", "{31EB654C-B562-73B4-2456-78FA875515D2}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CodeAnalysis.ExternalAccess.Extensions", "src\Tools\ExternalAccess\Extensions\Microsoft.CodeAnalysis.ExternalAccess.Extensions.csproj", "{6C816C16-D563-884A-D65B-5E68C6FB6659}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1567,6 +1569,14 @@ Global
{A8D5CFFA-7F9E-C35B-4F19-D63F6EC1D5CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A8D5CFFA-7F9E-C35B-4F19-D63F6EC1D5CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A8D5CFFA-7F9E-C35B-4F19-D63F6EC1D5CA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {068CD9AA-CEC3-CA68-1BAB-2B1B9FD711D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {068CD9AA-CEC3-CA68-1BAB-2B1B9FD711D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {068CD9AA-CEC3-CA68-1BAB-2B1B9FD711D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {068CD9AA-CEC3-CA68-1BAB-2B1B9FD711D3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D5A8E20C-E8D2-4A57-906A-263994D8731D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D5A8E20C-E8D2-4A57-906A-263994D8731D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D5A8E20C-E8D2-4A57-906A-263994D8731D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D5A8E20C-E8D2-4A57-906A-263994D8731D}.Release|Any CPU.Build.0 = Release|Any CPU
{9EB058F3-10C9-8F3F-AD9E-72CB362A0928}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9EB058F3-10C9-8F3F-AD9E-72CB362A0928}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9EB058F3-10C9-8F3F-AD9E-72CB362A0928}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -1787,14 +1797,10 @@ Global
{31EB654C-B562-73B4-2456-78FA875515D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{31EB654C-B562-73B4-2456-78FA875515D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{31EB654C-B562-73B4-2456-78FA875515D2}.Release|Any CPU.Build.0 = Release|Any CPU
- {068CD9AA-CEC3-CA68-1BAB-2B1B9FD711D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {068CD9AA-CEC3-CA68-1BAB-2B1B9FD711D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {068CD9AA-CEC3-CA68-1BAB-2B1B9FD711D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {068CD9AA-CEC3-CA68-1BAB-2B1B9FD711D3}.Release|Any CPU.Build.0 = Release|Any CPU
- {D5A8E20C-E8D2-4A57-906A-263994D8731D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D5A8E20C-E8D2-4A57-906A-263994D8731D}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D5A8E20C-E8D2-4A57-906A-263994D8731D}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D5A8E20C-E8D2-4A57-906A-263994D8731D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6C816C16-D563-884A-D65B-5E68C6FB6659}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6C816C16-D563-884A-D65B-5E68C6FB6659}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6C816C16-D563-884A-D65B-5E68C6FB6659}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6C816C16-D563-884A-D65B-5E68C6FB6659}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -2061,6 +2067,9 @@ Global
{2559DAF9-784E-4C29-E0E1-70204B1FD56E} = {C2D1346B-9665-4150-B644-075CF1636BAA}
{BD974609-C68B-4BE6-9682-EB132462B50D} = {C2D1346B-9665-4150-B644-075CF1636BAA}
{A8D5CFFA-7F9E-C35B-4F19-D63F6EC1D5CA} = {C2D1346B-9665-4150-B644-075CF1636BAA}
+ {068CD9AA-CEC3-CA68-1BAB-2B1B9FD711D3} = {8977A560-45C2-4EC2-A849-97335B382C74}
+ {4853A78A-4EC4-4D86-9F02-D0DDEAE85520} = {8977A560-45C2-4EC2-A849-97335B382C74}
+ {D5A8E20C-E8D2-4A57-906A-263994D8731D} = {8977A560-45C2-4EC2-A849-97335B382C74}
{9EB058F3-10C9-8F3F-AD9E-72CB362A0928} = {5880FECB-91F1-4AB8-8726-75EAFA8A918E}
{8087BDE4-6707-05A5-5F84-DFE6628E8EC8} = {6FE0875A-E178-4766-BCC6-87D37F273102}
{B095320B-6854-EBEC-98DB-26E06D97183A} = {6FE0875A-E178-4766-BCC6-87D37F273102}
@@ -2133,9 +2142,7 @@ Global
{A1EBC0BC-D378-B9B7-7A57-EF7DF11E3ED7} = {482C1FC7-4FD6-4381-8078-73BEBFAF4349}
{29080628-23A6-1DCB-F15E-93F1D1682CC1} = {482C1FC7-4FD6-4381-8078-73BEBFAF4349}
{31EB654C-B562-73B4-2456-78FA875515D2} = {0DDCFE67-7D4E-4709-9882-EC032A031789}
- {068CD9AA-CEC3-CA68-1BAB-2B1B9FD711D3} = {8977A560-45C2-4EC2-A849-97335B382C74}
- {4853A78A-4EC4-4D86-9F02-D0DDEAE85520} = {8977A560-45C2-4EC2-A849-97335B382C74}
- {D5A8E20C-E8D2-4A57-906A-263994D8731D} = {8977A560-45C2-4EC2-A849-97335B382C74}
+ {6C816C16-D563-884A-D65B-5E68C6FB6659} = {8977A560-45C2-4EC2-A849-97335B382C74}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {604E6B91-7BC0-4126-AE07-D4D2FEFC3D29}
@@ -2145,10 +2152,10 @@ Global
src\Workspaces\SharedUtilitiesAndExtensions\Workspace\VisualBasic\VisualBasicWorkspaceExtensions.projitems*{0141285d-8f6c-42c7-baf3-3c0ccd61c716}*SharedItemsImports = 5
src\Compilers\CSharp\csc\CscCommandLine.projitems*{0161e25c-918a-4dc8-9648-30fdcc8e31e9}*SharedItemsImports = 5
src\Tools\ExternalAccess\Razor\Shared\Microsoft.CodeAnalysis.ExternalAccess.Razor.Shared.projitems*{068cd9aa-cec3-ca68-1bab-2b1b9fd711d3}*SharedItemsImports = 5
- src\Dependencies\Contracts\Microsoft.CodeAnalysis.Contracts.projitems*{0c2e1633-1462-4712-88f4-a0c945bad3a8}*SharedItemsImports = 5
src\RoslynAnalyzers\Utilities\Compiler\Analyzer.Utilities.projitems*{08735294-3e6b-4420-9916-e7b8c4eb874d}*SharedItemsImports = 13
src\RoslynAnalyzers\Utilities\Compiler\Analyzer.Utilities.projitems*{0a1267e9-52ff-b8de-8522-802be55f41da}*SharedItemsImports = 5
src\RoslynAnalyzers\Utilities\FlowAnalysis\FlowAnalysis.Utilities.projitems*{0a1267e9-52ff-b8de-8522-802be55f41da}*SharedItemsImports = 5
+ src\Dependencies\Contracts\Microsoft.CodeAnalysis.Contracts.projitems*{0c2e1633-1462-4712-88f4-a0c945bad3a8}*SharedItemsImports = 5
src\Analyzers\Core\CodeFixes\CodeFixes.projitems*{1b6c4a1a-413b-41fb-9f85-5c09118e541b}*SharedItemsImports = 13
src\Compilers\Core\AnalyzerDriver\AnalyzerDriver.projitems*{1ee8cad3-55f9-4d91-96b2-084641da9a6c}*SharedItemsImports = 5
src\Dependencies\CodeAnalysis.Debugging\Microsoft.CodeAnalysis.Debugging.projitems*{1ee8cad3-55f9-4d91-96b2-084641da9a6c}*SharedItemsImports = 5
@@ -2177,9 +2184,9 @@ Global
src\Analyzers\CSharp\Analyzers\CSharpAnalyzers.projitems*{3973b09a-4fbf-44a5-8359-3d22ceb71f71}*SharedItemsImports = 5
src\Analyzers\CSharp\CodeFixes\CSharpCodeFixes.projitems*{3973b09a-4fbf-44a5-8359-3d22ceb71f71}*SharedItemsImports = 5
src\Compilers\CSharp\CSharpAnalyzerDriver\CSharpAnalyzerDriver.projitems*{3973b09a-4fbf-44a5-8359-3d22ceb71f71}*SharedItemsImports = 5
- src\Dependencies\Contracts\Microsoft.CodeAnalysis.Contracts.projitems*{41ed1bfa-fdad-4fe4-8118-db23fb49b0b0}*SharedItemsImports = 5
src\RoslynAnalyzers\Utilities\Compiler\Analyzer.Utilities.projitems*{3f65d29d-ed95-2d6f-b927-47c5cf070aa5}*SharedItemsImports = 5
src\RoslynAnalyzers\Utilities\Workspaces\Workspaces.Utilities.projitems*{3f65d29d-ed95-2d6f-b927-47c5cf070aa5}*SharedItemsImports = 5
+ src\Dependencies\Contracts\Microsoft.CodeAnalysis.Contracts.projitems*{41ed1bfa-fdad-4fe4-8118-db23fb49b0b0}*SharedItemsImports = 5
src\Workspaces\SharedUtilitiesAndExtensions\Workspace\CSharp\CSharpWorkspaceExtensions.projitems*{438db8af-f3f0-4ed9-80b5-13fddd5b8787}*SharedItemsImports = 13
src\Tools\ExternalAccess\Razor\Shared\Microsoft.CodeAnalysis.ExternalAccess.Razor.Shared.projitems*{4853a78a-4ec4-4d86-9f02-d0ddeae85520}*SharedItemsImports = 13
src\Compilers\CSharp\csc\CscCommandLine.projitems*{4b45ca0c-03a0-400f-b454-3d4bcb16af38}*SharedItemsImports = 5
@@ -2279,8 +2286,8 @@ Global
src\Dependencies\CodeAnalysis.Debugging\Microsoft.CodeAnalysis.Debugging.projitems*{edc68a0e-c68d-4a74-91b7-bf38ec909888}*SharedItemsImports = 5
src\Dependencies\Contracts\Microsoft.CodeAnalysis.Contracts.projitems*{fa0e905d-ec46-466d-b7b2-3b5557f9428c}*SharedItemsImports = 5
src\ExpressionEvaluator\Core\Source\ResultProvider\ResultProvider.projitems*{fa0e905d-ec46-466d-b7b2-3b5557f9428c}*SharedItemsImports = 5
- src\Dependencies\Contracts\Microsoft.CodeAnalysis.Contracts.projitems*{fce88bbd-9bbd-4871-b9b0-de176d73a6b0}*SharedItemsImports = 5
src\RoslynAnalyzers\Utilities\FlowAnalysis\FlowAnalysis.Utilities.projitems*{fcb56cba-fa35-46a8-86b7-bae5433197d9}*SharedItemsImports = 13
src\RoslynAnalyzers\Utilities\Compiler\Analyzer.Utilities.projitems*{fce0046b-03f8-78f6-86a1-8ddcee8f4c9f}*SharedItemsImports = 5
+ src\Dependencies\Contracts\Microsoft.CodeAnalysis.Contracts.projitems*{fce88bbd-9bbd-4871-b9b0-de176d73a6b0}*SharedItemsImports = 5
EndGlobalSection
EndGlobal
diff --git a/eng/config/PublishData.json b/eng/config/PublishData.json
index 0de7127d64bac..8c5e46b15deee 100644
--- a/eng/config/PublishData.json
+++ b/eng/config/PublishData.json
@@ -71,6 +71,7 @@
"Microsoft.CodeAnalysis.ExternalAccess.CompilerDeveloperSDK": "vs-impl",
"Microsoft.CodeAnalysis.ExternalAccess.Debugger": "vs-impl",
"Microsoft.CodeAnalysis.ExternalAccess.EditorConfigGenerator": "vs-impl",
+ "Microsoft.CodeAnalysis.ExternalAccess.Extensions": "vs-impl",
"Microsoft.CodeAnalysis.ExternalAccess.FSharp": "vs-impl",
"Microsoft.CodeAnalysis.ExternalAccess.IntelliTrace": "vs-impl",
"Microsoft.CodeAnalysis.ExternalAccess.ProjectSystem": "vs-impl",
diff --git a/eng/targets/Services.props b/eng/targets/Services.props
index 590cd0c2a1518..409f1b282f9a1 100644
--- a/eng/targets/Services.props
+++ b/eng/targets/Services.props
@@ -22,6 +22,7 @@
+
diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptLifeCycleManager.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptLifeCycleManager.cs
new file mode 100644
index 0000000000000..1aa9d2b7f0467
--- /dev/null
+++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptLifeCycleManager.cs
@@ -0,0 +1,21 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Composition;
+using Microsoft.CodeAnalysis.Host.Mef;
+using Microsoft.CodeAnalysis.LanguageServer;
+using Microsoft.CodeAnalysis.LanguageServer.Handler.ServerLifetime;
+
+namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript;
+
+[ExportLspServiceFactory(typeof(LspServiceLifeCycleManager), ProtocolConstants.TypeScriptLanguageContract), Shared]
+internal class VSTypeScriptLifeCycleManager : LspServiceLifeCycleManager.LspLifeCycleManagerFactory
+{
+ [ImportingConstructor]
+ [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
+ public VSTypeScriptLifeCycleManager(LspWorkspaceRegistrationService lspWorkspaceRegistrationService) : base(lspWorkspaceRegistrationService)
+ {
+ }
+}
diff --git a/src/Features/Core/Portable/Extensions/ExtensionFolder.cs b/src/Features/Core/Portable/Extensions/ExtensionFolder.cs
new file mode 100644
index 0000000000000..9b03247f9de6a
--- /dev/null
+++ b/src/Features/Core/Portable/Extensions/ExtensionFolder.cs
@@ -0,0 +1,183 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Immutable;
+using System.IO;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.ErrorReporting;
+using Microsoft.CodeAnalysis.Host;
+using Microsoft.CodeAnalysis.PooledObjects;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.Extensions;
+
+internal sealed partial class ExtensionMessageHandlerServiceFactory
+{
+ private partial class ExtensionMessageHandlerService
+ {
+ ///
+ /// Represents a folder that many individual extension assemblies can be loaded from.
+ ///
+ private sealed class ExtensionFolder
+ {
+ private readonly ExtensionMessageHandlerService _extensionMessageHandlerService;
+
+ ///
+ /// Lazily computed assembly loader for this particular folder.
+ ///
+ private readonly AsyncLazy _lazyAssemblyLoader;
+
+ ///
+ /// Mapping from assembly file path to the handlers it contains. Should only be mutated while the lock is held by our parent .
+ ///
+ private ImmutableDictionary> _assemblyFilePathToHandlers = ImmutableDictionary>.Empty;
+
+ public ExtensionFolder(
+ ExtensionMessageHandlerService extensionMessageHandlerService,
+ string assemblyFolderPath)
+ {
+ _extensionMessageHandlerService = extensionMessageHandlerService;
+ _lazyAssemblyLoader = AsyncLazy.Create(cancellationToken =>
+ {
+#if NET
+ var analyzerAssemblyLoaderProvider = _extensionMessageHandlerService._solutionServices.GetRequiredService();
+ var analyzerAssemblyLoader = analyzerAssemblyLoaderProvider.CreateNewShadowCopyLoader();
+
+ // Allow this assembly loader to load any dll in assemblyFolderPath.
+ foreach (var dll in Directory.EnumerateFiles(assemblyFolderPath, "*.dll"))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ try
+ {
+ // Check if the file is a valid .NET assembly.
+ AssemblyName.GetAssemblyName(dll);
+ }
+ catch
+ {
+ // The file is not a valid .NET assembly, skip it.
+ continue;
+ }
+
+ analyzerAssemblyLoader.AddDependencyLocation(dll);
+ }
+
+ return (IAnalyzerAssemblyLoaderInternal?)analyzerAssemblyLoader;
+#else
+ return (IAnalyzerAssemblyLoaderInternal?)null;
+#endif
+ });
+ }
+
+ public void Unload()
+ {
+ // Only if we've created the assembly loader do we need to do anything.
+ _lazyAssemblyLoader.TryGetValue(out var loader);
+ loader?.Dispose();
+ }
+
+ private async Task CreateAssemblyHandlersAsync(
+ string assemblyFilePath, CancellationToken cancellationToken)
+ {
+ // On NetFramework do nothing. We have no way to load extensions safely.
+ var analyzerAssemblyLoader = await _lazyAssemblyLoader.GetValueAsync(cancellationToken).ConfigureAwait(false);
+ if (analyzerAssemblyLoader is null)
+ {
+ return new(
+ DocumentMessageHandlers: ImmutableDictionary.Empty,
+ WorkspaceMessageHandlers: ImmutableDictionary.Empty);
+ }
+
+ var assembly = analyzerAssemblyLoader.LoadFromPath(assemblyFilePath);
+ var factory = _extensionMessageHandlerService._customMessageHandlerFactory;
+ Contract.ThrowIfNull(factory);
+
+ var documentMessageHandlers = factory
+ .CreateDocumentMessageHandlers(assembly, extensionIdentifier: assemblyFilePath, cancellationToken)
+ .ToImmutableDictionary(h => h.Name, h => (IExtensionMessageHandlerWrapper)h);
+ var workspaceMessageHandlers = factory
+ .CreateWorkspaceMessageHandlers(assembly, extensionIdentifier: assemblyFilePath, cancellationToken)
+ .ToImmutableDictionary(h => h.Name, h => (IExtensionMessageHandlerWrapper)h);
+
+ return new(documentMessageHandlers, workspaceMessageHandlers);
+ }
+
+ public void RegisterAssembly(string assemblyFilePath)
+ {
+ // Must be called under our parent's lock to ensure we see a consistent state of things.
+ // This allows us to safely examine our current state, and then add the new item.
+ Contract.ThrowIfTrue(!Monitor.IsEntered(_extensionMessageHandlerService._gate));
+
+ if (_assemblyFilePathToHandlers.ContainsKey(assemblyFilePath))
+ throw new InvalidOperationException($"Extension '{assemblyFilePath}' is already registered.");
+
+ _assemblyFilePathToHandlers = _assemblyFilePathToHandlers.Add(
+ assemblyFilePath,
+ AsyncLazy.Create(
+ cancellationToken => this.CreateAssemblyHandlersAsync(assemblyFilePath, cancellationToken)));
+ }
+
+ ///
+ /// Unregisters this assembly path from this extension folder. If this was the last registered path, then this
+ /// will return true so that this folder can be unloaded.
+ ///
+ public bool UnregisterAssembly(string assemblyFilePath)
+ {
+ // Must be called under our parent's lock to ensure we see a consistent state of things. This allows us
+ // to safely examine our current state, remove the existing item, and then return if we are now empty.
+ Contract.ThrowIfTrue(!Monitor.IsEntered(_extensionMessageHandlerService._gate));
+
+ if (!_assemblyFilePathToHandlers.ContainsKey(assemblyFilePath))
+ throw new InvalidOperationException($"Extension '{assemblyFilePath}' was not registered.");
+
+ _assemblyFilePathToHandlers = _assemblyFilePathToHandlers.Remove(assemblyFilePath);
+ return _assemblyFilePathToHandlers.Count == 0;
+ }
+
+ public async ValueTask GetExtensionMessageNamesAsync(string assemblyFilePath, CancellationToken cancellationToken)
+ {
+ // This is safe to do as our general contract is that all handler operations should be called explicitly
+ // between calls to Register/Unregister the extension. So this cannot race with an extension being
+ // removed.
+ if (!_assemblyFilePathToHandlers.TryGetValue(assemblyFilePath, out var lazyHandlers))
+ throw new InvalidOperationException($"Extension '{assemblyFilePath}' was not registered.");
+
+ // If loading the assembly and getting the handlers failed, then we will throw that exception outwards
+ // for the client to hear about.
+ var handlers = await lazyHandlers.GetValueAsync(cancellationToken).ConfigureAwait(false);
+
+ return new(
+ WorkspaceMessageHandlers: [.. handlers.WorkspaceMessageHandlers.Keys],
+ DocumentMessageHandlers: [.. handlers.DocumentMessageHandlers.Keys]);
+ }
+
+ public async ValueTask AddHandlersAsync(string messageName, bool isSolution, ArrayBuilder result, CancellationToken cancellationToken)
+ {
+ foreach (var (_, lazyHandler) in _assemblyFilePathToHandlers)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ AssemblyMessageHandlers handlers;
+ try
+ {
+ handlers = await lazyHandler.GetValueAsync(cancellationToken).ConfigureAwait(false);
+ }
+ catch (Exception ex) when (FatalError.ReportAndCatchUnlessCanceled(ex))
+ {
+ // If loading the assembly and getting the handlers failed, then we will ignore this assembly
+ // and continue on to the next one.
+ continue;
+ }
+
+ var specificHandlers = isSolution ? handlers.WorkspaceMessageHandlers : handlers.DocumentMessageHandlers;
+ if (specificHandlers.TryGetValue(messageName, out var handler))
+ result.Add(handler);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Features/Core/Portable/Extensions/ExtensionMessageHandlerService.cs b/src/Features/Core/Portable/Extensions/ExtensionMessageHandlerService.cs
new file mode 100644
index 0000000000000..eed864b297da1
--- /dev/null
+++ b/src/Features/Core/Portable/Extensions/ExtensionMessageHandlerService.cs
@@ -0,0 +1,194 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.IO;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Host;
+using Microsoft.CodeAnalysis.PooledObjects;
+using Microsoft.CodeAnalysis.Threading;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.Extensions;
+
+internal sealed partial class ExtensionMessageHandlerServiceFactory
+{
+ private readonly record struct AssemblyMessageHandlers(
+ ImmutableDictionary DocumentMessageHandlers,
+ ImmutableDictionary WorkspaceMessageHandlers);
+
+ private sealed partial class ExtensionMessageHandlerService(
+ SolutionServices solutionServices,
+ IExtensionMessageHandlerFactory? customMessageHandlerFactory)
+ : IExtensionMessageHandlerService
+ {
+ private readonly SolutionServices _solutionServices = solutionServices;
+ private readonly IExtensionMessageHandlerFactory? _customMessageHandlerFactory = customMessageHandlerFactory;
+
+ ///
+ /// Lock for , , and . Note: this type is designed such that all time while this lock is held
+ /// should be minimal. Importantly, no async work or IO should be done while holding this lock. Instead,
+ /// all of that work should be pushed into AsyncLazy values that compute when asked, outside of this lock.
+ ///
+ private readonly object _gate = new();
+
+ ///
+ /// Extensions assembly load contexts and loaded handlers, indexed by extension folder path.
+ ///
+ private ImmutableDictionary _folderPathToExtensionFolder = ImmutableDictionary.Empty;
+
+ ///
+ /// Cached handlers of document-related messages, indexed by handler message name.
+ ///
+ private readonly Dictionary>> _cachedDocumentHandlers_useOnlyUnderLock = new();
+
+ ///
+ /// Cached handlers of non-document-related messages, indexed by handler message name.
+ ///
+ private readonly Dictionary>> _cachedWorkspaceHandlers_useOnlyUnderLock = new();
+
+ private static string GetAssemblyFolderPath(string assemblyFilePath)
+ {
+ return Path.GetDirectoryName(assemblyFilePath)
+ ?? throw new InvalidOperationException($"Unable to get the directory name for {assemblyFilePath}.");
+ }
+
+ private void ClearCachedHandlers_WhileUnderLock()
+ {
+ Contract.ThrowIfTrue(!Monitor.IsEntered(_gate));
+ _cachedWorkspaceHandlers_useOnlyUnderLock.Clear();
+ _cachedDocumentHandlers_useOnlyUnderLock.Clear();
+ }
+
+ private ValueTask RegisterExtensionInCurrentProcessAsync(string assemblyFilePath)
+ {
+ var assemblyFolderPath = GetAssemblyFolderPath(assemblyFilePath);
+
+ // Take lock as we both want to update our state, and the state of the ExtensionFolder instance we get back.
+ lock (_gate)
+ {
+ // Clear out the cached handler names. They will be recomputed the next time we need them.
+ ClearCachedHandlers_WhileUnderLock();
+
+ var extensionFolder = ImmutableInterlocked.GetOrAdd(
+ ref _folderPathToExtensionFolder,
+ assemblyFolderPath,
+ assemblyFolderPath => new ExtensionFolder(this, assemblyFolderPath));
+
+ extensionFolder.RegisterAssembly(assemblyFilePath);
+ return default;
+ }
+ }
+
+ private ValueTask UnregisterExtensionInCurrentProcessAsync(string assemblyFilePath)
+ {
+ var folderToUnload = Unregister();
+
+ // If we're done with the folder, ask it to unload any resources it is holding onto. This will ask it
+ // to unload all ALCs needed to load it and the extensions within. Unloading will happen once the
+ // runtime/gc determine the ALC is finally collectible.
+ folderToUnload?.Unload();
+ return default;
+
+ ExtensionFolder? Unregister()
+ {
+ var assemblyFolderPath = GetAssemblyFolderPath(assemblyFilePath);
+
+ // Take lock as we both want to update our state, and the state of the ExtensionFolder instance we get back.
+ lock (_gate)
+ {
+ if (!_folderPathToExtensionFolder.TryGetValue(assemblyFolderPath, out var extensionFolder))
+ throw new InvalidOperationException($"No extension registered as '{assemblyFolderPath}'");
+
+ // Clear out the cached handler names. They will be recomputed the next time we need them.
+ ClearCachedHandlers_WhileUnderLock();
+
+ // If we're not done with the folder. Return null so our caller doesn't unload anything.
+ if (!extensionFolder.UnregisterAssembly(assemblyFilePath))
+ return null;
+
+ // Last extension in the folder. Remove our folder registration entirely, and return it so the
+ // caller can unload it.
+ _folderPathToExtensionFolder = _folderPathToExtensionFolder.Remove(assemblyFolderPath);
+ return extensionFolder;
+ }
+ }
+ }
+
+ private ValueTask ResetInCurrentProcessAsync()
+ {
+ ImmutableDictionary oldFolderPathToExtensionFolder;
+ lock (_gate)
+ {
+ oldFolderPathToExtensionFolder = _folderPathToExtensionFolder;
+ _folderPathToExtensionFolder = ImmutableDictionary.Empty;
+ ClearCachedHandlers_WhileUnderLock();
+ }
+
+ foreach (var (_, folderToUnload) in oldFolderPathToExtensionFolder)
+ folderToUnload.Unload();
+
+ return default;
+ }
+
+ private async ValueTask GetExtensionMessageNamesInCurrentProcessAsync(
+ string assemblyFilePath,
+ CancellationToken cancellationToken)
+ {
+ var assemblyFolderPath = GetAssemblyFolderPath(assemblyFilePath);
+
+ if (!_folderPathToExtensionFolder.TryGetValue(assemblyFolderPath, out var extensionFolder))
+ throw new InvalidOperationException($"No extensions registered at '{assemblyFolderPath}'");
+
+ return await extensionFolder.GetExtensionMessageNamesAsync(assemblyFilePath, cancellationToken).ConfigureAwait(false);
+ }
+
+ private async ValueTask HandleExtensionMessageInCurrentProcessAsync(
+ TArgument executeArgument, bool isSolution, string messageName, string jsonMessage,
+ Dictionary>> cachedHandlers,
+ CancellationToken cancellationToken)
+ {
+ AsyncLazy> lazyHandlers;
+ lock (_gate)
+ {
+ // May be called a lot. So we use the non-allocating form of this lookup pattern.
+ lazyHandlers = cachedHandlers.GetOrAdd(
+ messageName,
+ static (messageName, arg) => AsyncLazy.Create(
+ static (arg, cancellationToken) => arg.@this.ComputeHandlersAsync(arg.messageName, arg.isSolution, cancellationToken),
+ (messageName, arg.@this, arg.isSolution)),
+ (messageName, @this: this, isSolution));
+ }
+
+ var handlers = await lazyHandlers.GetValueAsync(cancellationToken).ConfigureAwait(false);
+ if (handlers.Length == 0)
+ throw new InvalidOperationException($"No handler found for message {messageName}.");
+
+ if (handlers.Length > 1)
+ throw new InvalidOperationException($"Multiple handlers found for message {messageName}.");
+
+ var handler = (IExtensionMessageHandlerWrapper)handlers[0];
+
+ var message = JsonSerializer.Deserialize(jsonMessage, handler.MessageType);
+ var result = await handler.ExecuteAsync(message, executeArgument, cancellationToken).ConfigureAwait(false);
+ return JsonSerializer.Serialize(result, handler.ResponseType);
+ }
+
+ private async Task> ComputeHandlersAsync(
+ string messageName, bool isSolution, CancellationToken cancellationToken)
+ {
+ using var _ = ArrayBuilder.GetInstance(out var result);
+
+ foreach (var (_, extensionFolder) in _folderPathToExtensionFolder)
+ await extensionFolder.AddHandlersAsync(messageName, isSolution, result, cancellationToken).ConfigureAwait(false);
+
+ return result.ToImmutable();
+ }
+ }
+}
diff --git a/src/Features/Core/Portable/Extensions/ExtensionMessageHandlerServiceFactory.cs b/src/Features/Core/Portable/Extensions/ExtensionMessageHandlerServiceFactory.cs
new file mode 100644
index 0000000000000..323ae2edd4dfd
--- /dev/null
+++ b/src/Features/Core/Portable/Extensions/ExtensionMessageHandlerServiceFactory.cs
@@ -0,0 +1,21 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Composition;
+using Microsoft.CodeAnalysis.Host;
+using Microsoft.CodeAnalysis.Host.Mef;
+
+namespace Microsoft.CodeAnalysis.Extensions;
+
+[ExportWorkspaceServiceFactory(typeof(IExtensionMessageHandlerService)), Shared]
+[method: ImportingConstructor]
+[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
+internal sealed partial class ExtensionMessageHandlerServiceFactory(
+ [Import(AllowDefault = true)] IExtensionMessageHandlerFactory? customMessageHandlerFactory)
+ : IWorkspaceServiceFactory
+{
+ public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices)
+ => new ExtensionMessageHandlerService(workspaceServices.SolutionServices, customMessageHandlerFactory);
+}
diff --git a/src/Features/Core/Portable/Extensions/ExtensionMessageHandlerService_RemoteOrLocal.cs b/src/Features/Core/Portable/Extensions/ExtensionMessageHandlerService_RemoteOrLocal.cs
new file mode 100644
index 0000000000000..e987ea2a97985
--- /dev/null
+++ b/src/Features/Core/Portable/Extensions/ExtensionMessageHandlerService_RemoteOrLocal.cs
@@ -0,0 +1,138 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Remote;
+using Microsoft.CodeAnalysis.Threading;
+
+namespace Microsoft.CodeAnalysis.Extensions;
+
+internal sealed partial class ExtensionMessageHandlerServiceFactory
+{
+ private sealed partial class ExtensionMessageHandlerService
+ {
+ // Code for bifurcating calls to either the local or remote process.
+
+ private async ValueTask ExecuteActionInRemoteOrCurrentProcessAsync(
+ Solution? solution,
+ Func executeInProcessAsync,
+ Func executeOutOfProcessAsync,
+ TArg arg,
+ CancellationToken cancellationToken)
+ {
+ await ExecuteFuncInRemoteOrCurrentProcessAsync(
+ solution,
+ static async (localService, tuple, cancellationToken) =>
+ {
+ var (executeInProcessAsync, _, arg) = tuple;
+ await executeInProcessAsync(localService, arg, cancellationToken).ConfigureAwait(false);
+ return default(VoidResult);
+ },
+ static async (service, tuple, checksum, cancellationToken) =>
+ {
+ var (_, executeOutOfProcessAsync, arg) = tuple;
+ await executeOutOfProcessAsync(service, arg, checksum, cancellationToken).ConfigureAwait(false);
+ return default(VoidResult);
+ },
+ (executeInProcessAsync, executeOutOfProcessAsync, arg),
+ cancellationToken).ConfigureAwait(false);
+ }
+
+ private async ValueTask ExecuteFuncInRemoteOrCurrentProcessAsync(
+ Solution? solution,
+ Func> executeInProcessAsync,
+ Func> executeOutOfProcessAsync,
+ TArg arg,
+ CancellationToken cancellationToken)
+ {
+ var client = await RemoteHostClient.TryGetClientAsync(_solutionServices, cancellationToken).ConfigureAwait(false);
+ if (client is null)
+ return await executeInProcessAsync(this, arg, cancellationToken).ConfigureAwait(false);
+
+ var result = solution is null
+ ? await client.TryInvokeAsync(
+ (remoteService, cancellationToken) => executeOutOfProcessAsync(remoteService, arg, null, cancellationToken),
+ cancellationToken).ConfigureAwait(false)
+ : await client.TryInvokeAsync(
+ solution,
+ (remoteService, checksum, cancellationToken) => executeOutOfProcessAsync(remoteService, arg, checksum, cancellationToken),
+ cancellationToken).ConfigureAwait(false);
+
+ // If the remote call succeeded, this will have a valid value in it and can be returned. If it did not
+ // succeed then we will have already shown the user an error message stating there was an issue making
+ // the call, and it's fine for this to throw again, unwinding the stack up to the caller.
+ return result.Value;
+ }
+
+ public ValueTask RegisterExtensionAsync(string assemblyFilePath, CancellationToken cancellationToken)
+ => ExecuteActionInRemoteOrCurrentProcessAsync(
+ solution: null,
+ static (localService, assemblyFilePath, _) => localService.RegisterExtensionInCurrentProcessAsync(assemblyFilePath),
+ static (remoteService, assemblyFilePath, _, cancellationToken) => remoteService.RegisterExtensionAsync(assemblyFilePath, cancellationToken),
+ assemblyFilePath,
+ cancellationToken);
+
+ public ValueTask UnregisterExtensionAsync(string assemblyFilePath, CancellationToken cancellationToken)
+ => ExecuteActionInRemoteOrCurrentProcessAsync(
+ solution: null,
+ static (localService, assemblyFilePath, _) => localService.UnregisterExtensionInCurrentProcessAsync(assemblyFilePath),
+ static (remoteService, assemblyFilePath, _, cancellationToken) => remoteService.UnregisterExtensionAsync(assemblyFilePath, cancellationToken),
+ assemblyFilePath,
+ cancellationToken);
+
+ public ValueTask GetExtensionMessageNamesAsync(string assemblyFilePath, CancellationToken cancellationToken)
+ => ExecuteFuncInRemoteOrCurrentProcessAsync(
+ solution: null,
+ static (localService, assemblyFilePath, cancellationToken) => localService.GetExtensionMessageNamesInCurrentProcessAsync(assemblyFilePath, cancellationToken),
+ static (remoteService, assemblyFilePath, _, cancellationToken) => remoteService.GetExtensionMessageNamesAsync(assemblyFilePath, cancellationToken),
+ assemblyFilePath,
+ cancellationToken);
+
+ public ValueTask ResetAsync(CancellationToken cancellationToken)
+ => ExecuteActionInRemoteOrCurrentProcessAsync(
+ solution: null,
+ static (localService, _, _) => localService.ResetInCurrentProcessAsync(),
+ static (remoteService, _, _, cancellationToken) => remoteService.ResetAsync(cancellationToken),
+ default(VoidResult),
+ cancellationToken);
+
+ public ValueTask HandleExtensionWorkspaceMessageAsync(Solution solution, string messageName, string jsonMessage, CancellationToken cancellationToken)
+ => ExecuteFuncInRemoteOrCurrentProcessAsync(
+ solution,
+ static (localService, arg, cancellationToken) =>
+ {
+ var (solution, messageName, jsonMessage, handlers) = arg;
+ return localService.HandleExtensionMessageInCurrentProcessAsync(
+ executeArgument: solution, isSolution: true, messageName, jsonMessage, handlers, cancellationToken);
+ },
+ static (remoteService, arg, checksum, cancellationToken) =>
+ {
+ var (_, messageName, jsonMessage, _) = arg;
+ return remoteService.HandleExtensionWorkspaceMessageAsync(
+ checksum!.Value, messageName, jsonMessage, cancellationToken);
+ },
+ (solution, messageName, jsonMessage, _cachedWorkspaceHandlers_useOnlyUnderLock),
+ cancellationToken);
+
+ public ValueTask HandleExtensionDocumentMessageAsync(Document document, string messageName, string jsonMessage, CancellationToken cancellationToken)
+ => ExecuteFuncInRemoteOrCurrentProcessAsync(
+ document.Project.Solution,
+ static (localService, arg, cancellationToken) =>
+ {
+ var (document, messageName, jsonMessage, handlers) = arg;
+ return localService.HandleExtensionMessageInCurrentProcessAsync(
+ executeArgument: document, isSolution: false, messageName, jsonMessage, handlers, cancellationToken);
+ },
+ static (remoteService, arg, checksum, cancellationToken) =>
+ {
+ var (document, messageName, jsonMessage, _) = arg;
+ return remoteService.HandleExtensionDocumentMessageAsync(
+ checksum!.Value, messageName, jsonMessage, document.Id, cancellationToken);
+ },
+ (document, messageName, jsonMessage, _cachedDocumentHandlers_useOnlyUnderLock),
+ cancellationToken);
+ }
+}
diff --git a/src/Features/Core/Portable/Extensions/IExtensionMessageHandlerFactory.cs b/src/Features/Core/Portable/Extensions/IExtensionMessageHandlerFactory.cs
new file mode 100644
index 0000000000000..77a56b42c9275
--- /dev/null
+++ b/src/Features/Core/Portable/Extensions/IExtensionMessageHandlerFactory.cs
@@ -0,0 +1,35 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Immutable;
+using System.Reflection;
+using System.Threading;
+
+namespace Microsoft.CodeAnalysis.Extensions;
+
+///
+/// Factory for creating instances of extension message handlers.
+///
+internal interface IExtensionMessageHandlerFactory
+{
+ ///
+ /// Creates instances for each
+ /// IExtensionWorkspaceMessageHandler type in .
+ ///
+ /// The assembly to scan for handlers.
+ /// Unique identifier of the extension owning this handler.
+ /// May be called multiple times for the same instance.
+ ImmutableArray> CreateWorkspaceMessageHandlers(
+ Assembly assembly, string extensionIdentifier, CancellationToken cancellationToken);
+
+ ///
+ /// Creates instances for each
+ /// IExtensionDocumentMessageHandler type in .
+ ///
+ /// The assembly to scan for handlers.
+ /// Unique identifier of the extension owning this handler.
+ /// May be called multiple times for the same instance.
+ ImmutableArray> CreateDocumentMessageHandlers(
+ Assembly assembly, string extensionIdentifier, CancellationToken cancellationToken);
+}
diff --git a/src/Features/Core/Portable/Extensions/IExtensionMessageHandlerService.cs b/src/Features/Core/Portable/Extensions/IExtensionMessageHandlerService.cs
new file mode 100644
index 0000000000000..52902278d5d18
--- /dev/null
+++ b/src/Features/Core/Portable/Extensions/IExtensionMessageHandlerService.cs
@@ -0,0 +1,57 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Host;
+
+namespace Microsoft.CodeAnalysis.Extensions;
+
+///
+/// This service is used to register, unregister and execute extension message handlers.
+///
+internal interface IExtensionMessageHandlerService : IWorkspaceService
+{
+ ///
+ /// Registers extension message handlers from the specified assembly.
+ ///
+ /// The assembly to register and create message handlers from.
+ ValueTask RegisterExtensionAsync(string assemblyFilePath, CancellationToken cancellationToken);
+
+ ///
+ /// Unregisters extension message handlers previously registered from .
+ ///
+ /// The assembly for which handlers should be unregistered.
+ ValueTask UnregisterExtensionAsync(string assemblyFilePath, CancellationToken cancellationToken);
+
+ ///
+ /// Gets the message names supported by the extension specified by .
+ ///
+ ValueTask GetExtensionMessageNamesAsync(string assemblyFilePath, CancellationToken cancellationToken);
+
+ ///
+ /// Unregisters all extension message handlers.
+ ///
+ ValueTask ResetAsync(CancellationToken cancellationToken);
+
+ ///
+ /// Executes a non-document-specific extension message handler with the given message and solution.
+ ///
+ /// The solution the message refers to.
+ /// The name of the handler to execute. This is generally the full name of the type implementing the handler.
+ /// The json message to be passed to the handler.
+ /// The json message returned by the handler.
+ ValueTask HandleExtensionWorkspaceMessageAsync(
+ Solution solution, string messageName, string jsonMessage, CancellationToken cancellationToken);
+
+ ///
+ /// Executes a document-specific extension message handler with the given message and solution.
+ ///
+ /// The document the message refers to.
+ /// The name of the handler to execute. This is generally the full name of the type implementing the handler.
+ /// The json message to be passed to the handler.
+ /// The json message returned by the handler.
+ ValueTask HandleExtensionDocumentMessageAsync(
+ Document documentId, string messageName, string jsonMessage, CancellationToken cancellationToken);
+}
diff --git a/src/Features/Core/Portable/Extensions/IExtensionMessageHandlerWrapper.cs b/src/Features/Core/Portable/Extensions/IExtensionMessageHandlerWrapper.cs
new file mode 100644
index 0000000000000..9d8defc5bada3
--- /dev/null
+++ b/src/Features/Core/Portable/Extensions/IExtensionMessageHandlerWrapper.cs
@@ -0,0 +1,51 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.CodeAnalysis.Extensions;
+
+///
+/// Wrapper for an IExtensionWorkspaceMessageHandler or IExtensionDocumentMessageHandler
+/// as returned by .
+///
+internal interface IExtensionMessageHandlerWrapper
+{
+ ///
+ /// The type of object received as parameter by the extension message handler.
+ ///
+ Type MessageType { get; }
+
+ ///
+ /// The type of object returned as result by the extension message handler.
+ ///
+ Type ResponseType { get; }
+
+ ///
+ /// The name of the extension message handler. This is generally the full name of the class implementing the handler.
+ ///
+ string Name { get; }
+
+ ///
+ /// The identifier of the extension that this message handler belongs to.
+ ///
+ string ExtensionIdentifier { get; }
+}
+
+/// The type of object received as parameter by the extension message
+/// handler.
+internal interface IExtensionMessageHandlerWrapper : IExtensionMessageHandlerWrapper
+{
+ ///
+ /// Executes the extension message handler with the given message and document.
+ ///
+ /// An object of type to be passed
+ /// to the handler.
+ /// The argument the handler operates on.
+ /// An object of type returned by the message
+ /// handler.
+ Task