Skip to content

Expose snapshot of IWorkspaceServices off of Solution, instead of just off of Workspace. #62914

@CyrusNajmabadi

Description

@CyrusNajmabadi

Background and Motivation

The IDE team intends to make it an error (at least for ourselves) to access the Workspace instance off of a Solution instance. e.g. through solution.Workspace. The reason for this is that Workspace is a complex mutable object that is not safe to allow access to to compute-only services.

Similarly, there are domains where it is strictly a bug for to do anything with this workspace (especially mutation). For example, in OOP, trying to mutate the OOP workspace is effectively non-sensical and has no effect (changes certainly aren't roundtripped to the host). OOP acts as a simple, purely functionaly, query service that takes in snapshots, computes answers, and hands those answers back.

However, one main reason that services access solution.Workspace is simply to query for other services that are needed while htey are are processing. For example, getting to the formatting services, etc. etc. These data services are safe to access, but we'd like to make that possible without going through the full-fat-mutable workspace instance (which again we intend to add to our banned-api list).

To that end we want to move a close analog to the existing immutable services container (called HostWorkspaceServices and HostLanguageServices) to being obtainable off of the Solution and Project instances themselves not just through teh Workspace instance. These analogs (which we're calling HostSolutionServices and HostProjectServices respectively), are thin wrappers around the existing types, but which do not expose any of the mutable state or event-hookup code. THey're purely used as DI containers solely for getting services and nothhing else.

Existing code that does solution.Workspace.GetService<ISomeService>() just needs to then be updated to solution.GetService<ISomeService>().

This also helps oop as today we suffer from all remote solution snapshots sharing the same workspace instance. However, this cannot properly represent what is actually going on in a host like VS, where there may be multiple workspaces (for example, one for the main VSSolution, and one for Metadata-as-Source). By decoupling this, it becomes possible to synchronize all those solutions to oop, just where each solution gets the right set of cached services for its needs.

Proposed API

namespace Microsoft.CodeAnalysis
{
     public class Solution
     {
+        public HostSolutionServices Services { get; }
     }

     public class Project
     {
+        public HostProjectServices Services { get; }
     }

+    public sealed class HostSolutionServices
+    {
+        // has internal constructor.  not publicly instantiable
+
+        /// <inheritdoc cref="HostWorkspaceServices.GetService"/>
+        public TWorkspaceService? GetService<TWorkspaceService>() where TWorkspaceService : IWorkspaceService;
+
+        /// <inheritdoc cref="HostWorkspaceServices.GetRequiredService"/>
+        public TWorkspaceService GetRequiredService<TWorkspaceService>() where TWorkspaceService : IWorkspaceService;
+
+        /// <inheritdoc cref="HostWorkspaceServices.SupportedLanguages"/>
+        public IEnumerable<string> SupportedLanguages { get; }
+
+        /// <inheritdoc cref="HostWorkspaceServices.IsSupported"/>
+        public bool IsSupported(string languageName);
+
+        /// <summary>
+        /// Gets the <see cref="HostProjectServices"/> for the language name.
+        /// </summary>
+        /// <exception cref="NotSupportedException">Thrown if the language isn't supported.</exception>
+        public HostProjectServices GetProjectServices(string languageName);
+    }
+
+    public sealed class HostProjectServices
+    {
+        // has internal constructor.  not publicly instantiable
+
+        public HostSolutionServices SolutionServices { get; }
+
+        /// <inheritdoc cref="HostLanguageServices.Language"/>
+        public string Language { get; }
+
+        /// <inheritdoc cref="HostLanguageServices.GetService"/>
+        public TLanguageService? GetService<TLanguageService>() where TLanguageService : ILanguageService;
+
+        /// <inheritdoc cref="HostLanguageServices.GetRequiredService"/>
+        public TLanguageService GetRequiredService<TLanguageService>() where TLanguageService : ILanguageService;
+    }

Usage Examples

Before:

var formattingService = solution.Workspace.Services.GetLanguageServices(node.Language).GetRequiredService<IFormattingService>();

After:

var formattingService = solution.Services.GetProjectServices(node.Language).GetRequiredService<IFormattingService>();

Risks

None that we're aware of. This is already an understood part of our public API and is exposed off of Workspace. It has nicely encapsulated out our idea of stateless services, and now makes them available without having to go through the large mutable risk area.

Metadata

Metadata

Labels

Area-IDEConcept-APIThis issue involves adding, removing, clarification, or modification of an API.Feature Requestapi-approvedAPI was approved in API review, it can be implemented

Type

No type
No fields configured for issues without a type.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions