diff --git a/eng/targets/Services.props b/eng/targets/Services.props
index a48040a0c68c5..06fe61e59b733 100644
--- a/eng/targets/Services.props
+++ b/eng/targets/Services.props
@@ -49,5 +49,6 @@
-->
+
diff --git a/src/VisualStudio/Core/Def/ProjectSystem/BrokeredService/WorkspaceProject.cs b/src/VisualStudio/Core/Def/ProjectSystem/BrokeredService/WorkspaceProject.cs
new file mode 100644
index 0000000000000..01d0a6ee86e2f
--- /dev/null
+++ b/src/VisualStudio/Core/Def/ProjectSystem/BrokeredService/WorkspaceProject.cs
@@ -0,0 +1,146 @@
+// 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.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Remote.ProjectSystem;
+using Roslyn.Utilities;
+
+namespace Microsoft.VisualStudio.LanguageServices.ProjectSystem.BrokeredService
+{
+ internal sealed class WorkspaceProject : IWorkspaceProject
+ {
+ // For the sake of the in-proc implementation here, we're going to build this atop IWorkspaceProjectContext so semantics are preserved
+ // for a few edge cases. Once the project system has moved onto this directly, we can flatten the implementations out.
+ private readonly IWorkspaceProjectContext _project;
+
+ public WorkspaceProject(IWorkspaceProjectContext project)
+ {
+ _project = project;
+ }
+
+ public ValueTask DisposeAsync()
+ {
+ _project.Dispose();
+ return ValueTaskFactory.CompletedTask;
+ }
+
+ public async Task AddAdditionalFilesAsync(IReadOnlyList additionalFilePaths, CancellationToken cancellationToken)
+ {
+ await using var batch = _project.CreateBatchScope().ConfigureAwait(false);
+
+ foreach (var additionalFilePath in additionalFilePaths)
+ _project.AddAdditionalFile(additionalFilePath);
+ }
+
+ public async Task RemoveAdditionalFilesAsync(IReadOnlyList additionalFilePaths, CancellationToken cancellationToken)
+ {
+ await using var batch = _project.CreateBatchScope().ConfigureAwait(false);
+
+ foreach (var additionalFilePath in additionalFilePaths)
+ _project.RemoveAdditionalFile(additionalFilePath);
+ }
+
+ public async Task AddAnalyzerConfigFilesAsync(IReadOnlyList analyzerConfigPaths, CancellationToken cancellationToken)
+ {
+ await using var batch = _project.CreateBatchScope().ConfigureAwait(false);
+
+ foreach (var analyzerConfigPath in analyzerConfigPaths)
+ _project.AddAnalyzerConfigFile(analyzerConfigPath);
+ }
+ public async Task RemoveAnalyzerConfigFilesAsync(IReadOnlyList analyzerConfigPaths, CancellationToken cancellationToken)
+ {
+ await using var batch = _project.CreateBatchScope().ConfigureAwait(false);
+
+ foreach (var analyzerConfigPath in analyzerConfigPaths)
+ _project.RemoveAnalyzerConfigFile(analyzerConfigPath);
+ }
+
+ public async Task AddAnalyzerReferencesAsync(IReadOnlyList analyzerPaths, CancellationToken cancellationToken)
+ {
+ await using var batch = _project.CreateBatchScope().ConfigureAwait(false);
+
+ foreach (var analyzerPath in analyzerPaths)
+ _project.AddAnalyzerReference(analyzerPath);
+ }
+
+ public async Task RemoveAnalyzerReferencesAsync(IReadOnlyList analyzerPaths, CancellationToken cancellationToken)
+ {
+ await using var batch = _project.CreateBatchScope().ConfigureAwait(false);
+
+ foreach (var analyzerPath in analyzerPaths)
+ _project.RemoveAnalyzerReference(analyzerPath);
+ }
+
+ public async Task AddMetadataReferencesAsync(IReadOnlyList metadataReferences, CancellationToken cancellationToken)
+ {
+ await using var batch = _project.CreateBatchScope().ConfigureAwait(false);
+
+ foreach (var metadataReference in metadataReferences)
+ {
+ _project.AddMetadataReference(
+ metadataReference.FilePath,
+ new MetadataReferenceProperties(MetadataImageKind.Assembly, default, metadataReference.EmbedInteropTypes));
+ }
+ }
+
+ public async Task RemoveMetadataReferencesAsync(IReadOnlyList metadataReferences, CancellationToken cancellationToken)
+ {
+ await using var batch = _project.CreateBatchScope().ConfigureAwait(false);
+
+ // The existing IWorkspaceProjectContext API here is a bit odd in that it only looks at the file path, and trusts that there aren't two
+ // references with the same path but different properties.
+ foreach (var metadataReference in metadataReferences)
+ _project.RemoveMetadataReference(metadataReference.FilePath);
+ }
+
+ public async Task AddSourceFilesAsync(IReadOnlyList sourceFiles, CancellationToken cancellationToken)
+ {
+ await using var batch = _project.CreateBatchScope().ConfigureAwait(false);
+
+ foreach (var sourceFile in sourceFiles)
+ {
+ _project.AddSourceFile(
+ sourceFile.FilePath,
+ folderNames: sourceFile.FolderNames);
+ }
+ }
+ public async Task RemoveSourceFilesAsync(IReadOnlyList sourceFiles, CancellationToken cancellationToken)
+ {
+ await using var batch = _project.CreateBatchScope().ConfigureAwait(false);
+
+ foreach (var sourceFile in sourceFiles)
+ _project.RemoveSourceFile(sourceFile);
+ }
+
+ public async Task SetBuildSystemPropertiesAsync(IReadOnlyDictionary properties, CancellationToken cancellationToken)
+ {
+ await using var batch = _project.CreateBatchScope().ConfigureAwait(false);
+
+ foreach (var property in properties)
+ _project.SetProperty(property.Key, property.Value);
+ }
+
+ public Task SetCommandLineArgumentsAsync(IReadOnlyList arguments, CancellationToken cancellationToken)
+ {
+ _project.SetOptions(arguments.ToImmutableArray());
+ return Task.CompletedTask;
+ }
+
+ public Task SetDisplayNameAsync(string displayName, CancellationToken cancellationToken)
+ {
+ _project.DisplayName = displayName;
+ return Task.CompletedTask;
+ }
+
+ public Task StartBatchAsync(CancellationToken cancellationToken)
+ {
+ return Task.FromResult(_project.CreateBatchScope());
+ }
+ }
+}
diff --git a/src/VisualStudio/Core/Def/ProjectSystem/BrokeredService/WorkspaceProjectFactoryService.cs b/src/VisualStudio/Core/Def/ProjectSystem/BrokeredService/WorkspaceProjectFactoryService.cs
new file mode 100644
index 0000000000000..98a35c85eeb13
--- /dev/null
+++ b/src/VisualStudio/Core/Def/ProjectSystem/BrokeredService/WorkspaceProjectFactoryService.cs
@@ -0,0 +1,64 @@
+// 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.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Remote;
+using Microsoft.CodeAnalysis.Remote.ProjectSystem;
+using Microsoft.ServiceHub.Framework;
+
+namespace Microsoft.VisualStudio.LanguageServices.ProjectSystem.BrokeredService
+{
+ internal class WorkspaceProjectFactoryService : IWorkspaceProjectFactoryService
+ {
+ public const string ServiceName = "WorkspaceProjectFactoryService";
+ public static readonly ServiceDescriptor ServiceDescriptor = ServiceDescriptor.CreateInProcServiceDescriptor(ServiceDescriptors.ComponentName, ServiceName, suffix: "", ServiceDescriptors.GetFeatureDisplayName);
+
+ private readonly IWorkspaceProjectContextFactory _workspaceProjectContextFactory;
+
+ // For the sake of the in-proc implementation here, we're going to build this atop IWorkspaceProjectContext so semantics are preserved
+ // for a few edge cases. Once the project system has moved onto this directly, we can flatten the implementations out.
+ public WorkspaceProjectFactoryService(IWorkspaceProjectContextFactory workspaceProjectContextFactory)
+ {
+ _workspaceProjectContextFactory = workspaceProjectContextFactory;
+ }
+
+ public async Task CreateAndAddProjectAsync(WorkspaceProjectCreationInfo creationInfo, CancellationToken cancellationToken)
+ {
+ var project = await _workspaceProjectContextFactory.CreateProjectContextAsync(
+ Guid.NewGuid(), // TODO: figure out some other side-channel way of communicating this
+ creationInfo.DisplayName,
+ creationInfo.Language,
+ new EvaluationDataShim(creationInfo.BuildSystemProperties),
+ hostObject: null, // TODO: figure out some other side-channel way of communicating this
+ cancellationToken).ConfigureAwait(false);
+
+ return new WorkspaceProject(project);
+ }
+
+ public Task> GetSupportedBuildSystemPropertiesAsync(CancellationToken cancellationToken)
+ {
+ return Task.FromResult((IReadOnlyCollection)_workspaceProjectContextFactory.EvaluationItemNames);
+ }
+
+ private sealed class EvaluationDataShim : EvaluationData
+ {
+ private readonly IReadOnlyDictionary _buildSystemProperties;
+
+ public EvaluationDataShim(IReadOnlyDictionary buildSystemProperties)
+ {
+ _buildSystemProperties = buildSystemProperties;
+ }
+
+ public override string GetPropertyValue(string name)
+ {
+ return _buildSystemProperties.TryGetValue(name, out var value) ? value : "";
+ }
+ }
+ }
+}
diff --git a/src/VisualStudio/Core/Def/ProjectSystem/CPS/IWorkspaceProjectContext.cs b/src/VisualStudio/Core/Def/ProjectSystem/CPS/IWorkspaceProjectContext.cs
index 05e9271ef5c09..83e92eb485a32 100644
--- a/src/VisualStudio/Core/Def/ProjectSystem/CPS/IWorkspaceProjectContext.cs
+++ b/src/VisualStudio/Core/Def/ProjectSystem/CPS/IWorkspaceProjectContext.cs
@@ -9,6 +9,7 @@
using System.Collections.Immutable;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
+using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem;
namespace Microsoft.VisualStudio.LanguageServices.ProjectSystem
{
@@ -72,6 +73,7 @@ internal interface IWorkspaceProjectContext : IDisposable
void RemoveAnalyzerConfigFile(string filePath);
void StartBatch();
+ IAsyncDisposable CreateBatchScope();
ValueTask EndBatchAsync();
void ReorderSourceFiles(IEnumerable filePaths);
diff --git a/src/VisualStudio/Core/Def/RoslynPackage.cs b/src/VisualStudio/Core/Def/RoslynPackage.cs
index 130b7e3169592..d4f9140ad1d37 100644
--- a/src/VisualStudio/Core/Def/RoslynPackage.cs
+++ b/src/VisualStudio/Core/Def/RoslynPackage.cs
@@ -33,9 +33,12 @@
using Microsoft.VisualStudio.LanguageServices.Implementation.TableDataSource;
using Microsoft.VisualStudio.LanguageServices.Implementation.UnusedReferences;
using Microsoft.VisualStudio.LanguageServices.InheritanceMargin;
+using Microsoft.VisualStudio.LanguageServices.ProjectSystem;
+using Microsoft.VisualStudio.LanguageServices.ProjectSystem.BrokeredService;
using Microsoft.VisualStudio.LanguageServices.StackTraceExplorer;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
+using Microsoft.VisualStudio.Shell.ServiceBroker;
using Microsoft.VisualStudio.TaskStatusCenter;
using Microsoft.VisualStudio.TextManager.Interop;
using Microsoft.VisualStudio.Threading;
@@ -170,6 +173,13 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke
// doc events and appropriately map files to/from it and other relevant workspaces (like the
// metadata-as-source workspace).
await this.ComponentModel.GetService().InitializeAsync(this).ConfigureAwait(false);
+
+ // Proffer in-process service broker services
+ var serviceBrokerContainer = await this.GetServiceAsync(this.JoinableTaskFactory).ConfigureAwait(false);
+
+ serviceBrokerContainer.Proffer(
+ WorkspaceProjectFactoryService.ServiceDescriptor,
+ (_, _, _, _) => ValueTaskFactory.FromResult