File: MSBuild\MSBuildWorkspace.cs
Web Access
Project: src\src\Workspaces\Core\MSBuild\Microsoft.CodeAnalysis.Workspaces.MSBuild.csproj (Microsoft.CodeAnalysis.Workspaces.MSBuild)
// 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.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Build.Framework;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.MSBuild
{
    /// <summary>
    /// A workspace that can be populated by opening MSBuild solution and project files.
    /// </summary>
    public sealed class MSBuildWorkspace : Workspace
    {
        // used to serialize access to public methods
        private readonly NonReentrantLock _serializationLock = new();
 
        private readonly MSBuildProjectLoader _loader;
        private readonly Microsoft.Extensions.Logging.ILoggerFactory _loggerFactory;
        private readonly ProjectFileExtensionRegistry _projectFileExtensionRegistry;
        private readonly DiagnosticReporter _reporter;
 
        private MSBuildWorkspace(
            HostServices hostServices,
            ImmutableDictionary<string, string> properties)
            : base(hostServices, WorkspaceKind.MSBuild)
        {
            _reporter = new DiagnosticReporter(this);
            _projectFileExtensionRegistry = new ProjectFileExtensionRegistry(Services.SolutionServices, _reporter);
            _loggerFactory = DiagnosticReporterLoggerProvider.CreateLoggerFactoryForDiagnosticReporter(_reporter);
            _loader = new MSBuildProjectLoader(Services.SolutionServices, _reporter, _loggerFactory, _projectFileExtensionRegistry, properties);
        }
 
        /// <summary>
        /// Create a new instance of a workspace that can be populated by opening solution and project files.
        /// </summary>
        public static MSBuildWorkspace Create()
        {
            return Create(ImmutableDictionary<string, string>.Empty);
        }
 
        /// <summary>
        /// Create a new instance of a workspace that can be populated by opening solution and project files.
        /// </summary>
        /// <param name="properties">An optional set of MSBuild properties used when interpreting project files.
        /// These are the same properties that are passed to msbuild via the /property:&lt;n&gt;=&lt;v&gt; command line argument.</param>
        public static MSBuildWorkspace Create(IDictionary<string, string> properties)
        {
            return Create(properties, MSBuildMefHostServices.DefaultServices);
        }
 
        /// <summary>
        /// Create a new instance of a workspace that can be populated by opening solution and project files.
        /// </summary>
        /// <param name="hostServices">The <see cref="HostServices"/> used to configure this workspace.</param>
        public static MSBuildWorkspace Create(HostServices hostServices)
        {
            return Create(ImmutableDictionary<string, string>.Empty, hostServices);
        }
 
        /// <summary>
        /// Create a new instance of a workspace that can be populated by opening solution and project files.
        /// </summary>
        /// <param name="properties">The MSBuild properties used when interpreting project files.
        /// These are the same properties that are passed to msbuild via the /property:&lt;n&gt;=&lt;v&gt; command line argument.</param>
        /// <param name="hostServices">The <see cref="HostServices"/> used to configure this workspace.</param>
        public static MSBuildWorkspace Create(IDictionary<string, string> properties, HostServices hostServices)
        {
            if (properties == null)
            {
                throw new ArgumentNullException(nameof(properties));
            }
 
            if (hostServices == null)
            {
                throw new ArgumentNullException(nameof(hostServices));
            }
 
            return new MSBuildWorkspace(hostServices, properties.ToImmutableDictionary());
        }
 
        /// <summary>
        /// The MSBuild properties used when interpreting project files.
        /// These are the same properties that are passed to msbuild via the /property:&lt;n&gt;=&lt;v&gt; command line argument.
        /// </summary>
        public ImmutableDictionary<string, string> Properties => _loader.Properties;
 
        /// <summary>
        /// Diagnostics logged while opening solutions, projects and documents.
        /// </summary>
        public ImmutableList<WorkspaceDiagnostic> Diagnostics => _reporter.Diagnostics;
 
        protected internal override void OnWorkspaceFailed(WorkspaceDiagnostic diagnostic)
        {
            _reporter.AddDiagnostic(diagnostic);
            base.OnWorkspaceFailed(diagnostic);
        }
 
        /// <summary>
        /// Determines if metadata from existing output assemblies is loaded instead of opening referenced projects.
        /// If the referenced project is already opened, the metadata will not be loaded.
        /// If the metadata assembly cannot be found the referenced project will be opened instead.
        /// </summary>
        public bool LoadMetadataForReferencedProjects
        {
            get { return _loader.LoadMetadataForReferencedProjects; }
            set { _loader.LoadMetadataForReferencedProjects = value; }
        }
 
        /// <summary>
        /// Determines if unrecognized projects are skipped when solutions or projects are opened.
        /// 
        /// An project is unrecognized if it either has 
        ///   a) an invalid file path, 
        ///   b) a non-existent project file,
        ///   c) has an unrecognized file extension or 
        ///   d) a file extension associated with an unsupported language.
        /// 
        /// If unrecognized projects cannot be skipped a corresponding exception is thrown.
        /// </summary>
        public bool SkipUnrecognizedProjects
        {
            get => _loader.SkipUnrecognizedProjects;
            set => _loader.SkipUnrecognizedProjects = value;
        }
 
        /// <summary>
        /// Associates a project file extension with a language name.
        /// </summary>
        public void AssociateFileExtensionWithLanguage(string projectFileExtension, string language)
        {
            _loader.AssociateFileExtensionWithLanguage(projectFileExtension, language);
        }
 
        /// <summary>
        /// Close the open solution, and reset the workspace to a new empty solution.
        /// </summary>
        public void CloseSolution()
        {
            using (_serializationLock.DisposableWait())
            {
                this.ClearSolution();
            }
        }
 
        private static string GetAbsolutePath(string path, string baseDirectoryPath)
        {
            return Path.GetFullPath(FileUtilities.ResolveRelativePath(path, baseDirectoryPath) ?? path);
        }
 
        #region Open Solution & Project
        /// <summary>
        /// Open a solution file and all referenced projects.
        /// </summary>
        /// <param name="solutionFilePath">The path to the solution file to be opened. This may be an absolute path or a path relative to the
        /// current working directory.</param>
        /// <param name="progress">An optional <see cref="IProgress{T}"/> that will receive updates as the solution is opened.</param>
        /// <param name="cancellationToken">An optional <see cref="CancellationToken"/> to allow cancellation of this operation.</param>
#pragma warning disable RS0026 // Special case to avoid ILogger type getting loaded in downstream clients
        public Task<Solution> OpenSolutionAsync(
#pragma warning restore RS0026
            string solutionFilePath,
            IProgress<ProjectLoadProgress>? progress = null,
            CancellationToken cancellationToken = default)
            => OpenSolutionAsync(solutionFilePath, msbuildLogger: null, progress, cancellationToken);
 
        /// <summary>
        /// Open a solution file and all referenced projects.
        /// </summary>
        /// <param name="solutionFilePath">The path to the solution file to be opened. This may be an absolute path or a path relative to the
        /// current working directory.</param>
        /// <param name="progress">An optional <see cref="IProgress{T}"/> that will receive updates as the solution is opened.</param>
        /// <param name="msbuildLogger">An optional <see cref="ILogger"/> that will log msbuild results.</param>
        /// <param name="cancellationToken">An optional <see cref="CancellationToken"/> to allow cancellation of this operation.</param>
#pragma warning disable RS0026 // Special case to avoid ILogger type getting loaded in downstream clients
        public async Task<Solution> OpenSolutionAsync(
#pragma warning restore RS0026
            string solutionFilePath,
            ILogger? msbuildLogger,
            IProgress<ProjectLoadProgress>? progress = null,
            CancellationToken cancellationToken = default)
        {
            if (solutionFilePath == null)
            {
                throw new ArgumentNullException(nameof(solutionFilePath));
            }
 
            this.ClearSolution();
 
            var solutionInfo = await _loader.LoadSolutionInfoAsync(solutionFilePath, progress, msbuildLogger, cancellationToken).ConfigureAwait(false);
 
            // construct workspace from loaded project infos
            this.OnSolutionAdded(solutionInfo);
 
            this.UpdateReferencesAfterAdd();
 
            return this.CurrentSolution;
        }
 
        /// <summary>
        /// Open a project file and all referenced projects.
        /// </summary>
        /// <param name="projectFilePath">The path to the project file to be opened. This may be an absolute path or a path relative to the
        /// current working directory.</param>
        /// <param name="progress">An optional <see cref="IProgress{T}"/> that will receive updates as the project is opened.</param>
        /// <param name="cancellationToken">An optional <see cref="CancellationToken"/> to allow cancellation of this operation.</param>
#pragma warning disable RS0026 // Special case to avoid ILogger type getting loaded in downstream clients
        public Task<Project> OpenProjectAsync(
#pragma warning restore RS0026
            string projectFilePath,
            IProgress<ProjectLoadProgress>? progress = null,
            CancellationToken cancellationToken = default)
            => OpenProjectAsync(projectFilePath, msbuildLogger: null, progress, cancellationToken);
 
        /// <summary>
        /// Open a project file and all referenced projects.
        /// </summary>
        /// <param name="projectFilePath">The path to the project file to be opened. This may be an absolute path or a path relative to the
        /// current working directory.</param>
        /// <param name="progress">An optional <see cref="IProgress{T}"/> that will receive updates as the project is opened.</param>
        /// <param name="msbuildLogger">An optional <see cref="ILogger"/> that will log msbuild results..</param>
        /// <param name="cancellationToken">An optional <see cref="CancellationToken"/> to allow cancellation of this operation.</param>
#pragma warning disable RS0026 // Special case to avoid ILogger type getting loaded in downstream clients
        public async Task<Project> OpenProjectAsync(
#pragma warning restore RS0026
            string projectFilePath,
            ILogger? msbuildLogger,
            IProgress<ProjectLoadProgress>? progress = null,
            CancellationToken cancellationToken = default)
        {
            if (projectFilePath == null)
            {
                throw new ArgumentNullException(nameof(projectFilePath));
            }
 
            var projectMap = ProjectMap.Create(this.CurrentSolution);
            var projects = await _loader.LoadProjectInfoAsync(projectFilePath, projectMap, progress, msbuildLogger, cancellationToken).ConfigureAwait(false);
 
            // add projects to solution
            foreach (var project in projects)
            {
                this.OnProjectAdded(project);
            }
 
            this.UpdateReferencesAfterAdd();
 
            var projectResult = this.CurrentSolution.GetProject(projects[0].Id);
            RoslynDebug.AssertNotNull(projectResult);
            return projectResult;
        }
 
        #endregion
 
        #region Apply Changes
        public override bool CanApplyChange(ApplyChangesKind feature)
        {
            return feature is
                ApplyChangesKind.ChangeDocument or
                ApplyChangesKind.AddDocument or
                ApplyChangesKind.RemoveDocument or
                ApplyChangesKind.AddMetadataReference or
                ApplyChangesKind.RemoveMetadataReference or
                ApplyChangesKind.AddProjectReference or
                ApplyChangesKind.RemoveProjectReference or
                ApplyChangesKind.AddAnalyzerReference or
                ApplyChangesKind.RemoveAnalyzerReference or
                ApplyChangesKind.ChangeAdditionalDocument;
        }
 
        private static bool HasProjectFileChanges(ProjectChanges changes)
        {
            return changes.GetAddedDocuments().Any() ||
                   changes.GetRemovedDocuments().Any() ||
                   changes.GetAddedMetadataReferences().Any() ||
                   changes.GetRemovedMetadataReferences().Any() ||
                   changes.GetAddedProjectReferences().Any() ||
                   changes.GetRemovedProjectReferences().Any() ||
                   changes.GetAddedAnalyzerReferences().Any() ||
                   changes.GetRemovedAnalyzerReferences().Any();
        }
 
        private BuildHostProcessManager? _applyChangesBuildHostProcessManager;
 
        /// <summary>
        /// The loaded project file that we are currently applying changes to. This is set in <see cref="ApplyProjectChanges(ProjectChanges)"/> if we're modifying a project that is going to require
        /// file changes; it's cleared once we're done with that project.
        /// </summary>
        private RemoteProjectFile? _applyChangesProjectFile;
 
        public override bool TryApplyChanges(Solution newSolution)
        {
            return TryApplyChanges(newSolution, CodeAnalysisProgress.None);
        }
 
        internal override bool TryApplyChanges(Solution newSolution, IProgress<CodeAnalysisProgress> progressTracker)
        {
            using (_serializationLock.DisposableWait())
            {
                try
                {
                    Debug.Assert(_applyChangesBuildHostProcessManager == null);
                    _applyChangesBuildHostProcessManager = new BuildHostProcessManager(Properties, loggerFactory: _loggerFactory);
 
                    return base.TryApplyChanges(newSolution, progressTracker);
                }
                finally
                {
                    _applyChangesBuildHostProcessManager!.DisposeAsync().AsTask().Wait();
                    _applyChangesBuildHostProcessManager = null;
                }
            }
        }
 
        protected override void ApplyProjectChanges(ProjectChanges projectChanges)
        {
            Debug.Assert(_applyChangesBuildHostProcessManager != null);
            Debug.Assert(_applyChangesProjectFile == null);
 
            var project = projectChanges.OldProject ?? projectChanges.NewProject;
 
            try
            {
                // if we need to modify the project file, load it first.
                if (HasProjectFileChanges(projectChanges))
                {
                    var projectPath = project.FilePath;
                    if (projectPath is null)
                    {
                        _reporter.Report(new ProjectDiagnostic(WorkspaceDiagnosticKind.Failure,
                                                               string.Format(WorkspaceMSBuildResources.Project_path_for_0_was_null, project.Name),
                                                               projectChanges.ProjectId));
                        return;
                    }
 
                    if (_projectFileExtensionRegistry.TryGetLanguageNameFromProjectPath(projectPath, DiagnosticReportingMode.Log, out var languageName))
                    {
                        try
                        {
                            var buildHost = _applyChangesBuildHostProcessManager.GetBuildHostWithFallbackAsync(projectPath, CancellationToken.None).Result;
                            _applyChangesProjectFile = buildHost.LoadProjectFileAsync(projectPath, languageName, CancellationToken.None).Result;
                        }
                        catch (IOException exception)
                        {
                            _reporter.Report(new ProjectDiagnostic(WorkspaceDiagnosticKind.Failure, exception.Message, projectChanges.ProjectId));
                        }
                    }
                }
 
                // do normal apply operations
                base.ApplyProjectChanges(projectChanges);
 
                // save project file
                if (_applyChangesProjectFile != null)
                {
                    try
                    {
                        _applyChangesProjectFile.SaveAsync(CancellationToken.None).Wait();
                    }
                    catch (IOException exception)
                    {
                        _reporter.Report(new ProjectDiagnostic(WorkspaceDiagnosticKind.Failure, exception.Message, projectChanges.ProjectId));
                    }
                }
            }
            finally
            {
                _applyChangesProjectFile = null;
            }
        }
 
        protected override void ApplyDocumentTextChanged(DocumentId documentId, SourceText text)
        {
            var document = this.CurrentSolution.GetDocument(documentId);
            if (document != null)
            {
                var encoding = DetermineEncoding(text, document);
                if (document.FilePath is null)
                {
                    var message = string.Format(WorkspaceMSBuildResources.Path_for_document_0_was_null, document.Name);
                    _reporter.Report(new DocumentDiagnostic(WorkspaceDiagnosticKind.Failure, message, document.Id));
                    return;
                }
 
                this.SaveDocumentText(documentId, document.FilePath, text, encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
                this.OnDocumentTextChanged(documentId, text, PreservationMode.PreserveValue);
            }
        }
 
        protected override void ApplyAdditionalDocumentTextChanged(DocumentId documentId, SourceText text)
        {
            var document = this.CurrentSolution.GetAdditionalDocument(documentId);
            if (document != null)
            {
                var encoding = DetermineEncoding(text, document);
                if (document.FilePath is null)
                {
                    var message = string.Format(WorkspaceMSBuildResources.Path_for_additional_document_0_was_null, document.Name);
                    _reporter.Report(new DocumentDiagnostic(WorkspaceDiagnosticKind.Failure, message, document.Id));
                    return;
                }
 
                this.SaveDocumentText(documentId, document.FilePath, text, encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
                this.OnAdditionalDocumentTextChanged(documentId, text, PreservationMode.PreserveValue);
            }
        }
 
        private static Encoding? DetermineEncoding(SourceText text, TextDocument document)
        {
            if (text.Encoding != null)
            {
                return text.Encoding;
            }
 
            try
            {
                if (document.FilePath is null)
                {
                    return null;
                }
 
                using var stream = new FileStream(document.FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
                var onDiskText = EncodedStringText.Create(stream);
                return onDiskText.Encoding;
            }
            catch (IOException)
            {
            }
            catch (InvalidDataException)
            {
            }
 
            return null;
        }
 
        protected override void ApplyDocumentAdded(DocumentInfo info, SourceText text)
        {
            Debug.Assert(_applyChangesProjectFile != null);
 
            var project = this.CurrentSolution.GetRequiredProject(info.Id.ProjectId);
 
            var extension = project.Language switch
            {
                LanguageNames.CSharp => ".cs",
                LanguageNames.VisualBasic => ".vb",
                _ => throw ExceptionUtilities.UnexpectedValue(project.Language)
            };
 
            var fileName = Path.ChangeExtension(info.Name, extension);
 
            var relativePath = (info.Folders != null && info.Folders.Count > 0)
                ? Path.Combine(Path.Combine([.. info.Folders]), fileName)
                : fileName;
 
            var fullPath = GetAbsolutePath(relativePath, Path.GetDirectoryName(project.FilePath)!);
 
            var newDocumentInfo = info.WithName(fileName)
                .WithFilePath(fullPath)
                .WithTextLoader(new WorkspaceFileTextLoader(Services.SolutionServices, fullPath, text.Encoding));
 
            // add document to project file
            _applyChangesProjectFile.AddDocumentAsync(relativePath, logicalPath: null, CancellationToken.None).Wait();
 
            // add to solution
            this.OnDocumentAdded(newDocumentInfo);
 
            // save text to disk
            if (text != null)
            {
                this.SaveDocumentText(info.Id, fullPath, text, text.Encoding ?? Encoding.UTF8);
            }
        }
 
        private void SaveDocumentText(DocumentId id, string fullPath, SourceText newText, Encoding encoding)
        {
            try
            {
                var dir = Path.GetDirectoryName(fullPath);
                Contract.ThrowIfNull(dir);
 
                if (!Directory.Exists(dir))
                {
                    Directory.CreateDirectory(dir);
                }
 
                Debug.Assert(encoding != null);
                using var writer = new StreamWriter(fullPath, append: false, encoding: encoding);
                newText.Write(writer);
            }
            catch (IOException exception)
            {
                _reporter.Report(new DocumentDiagnostic(WorkspaceDiagnosticKind.Failure, exception.Message, id));
            }
        }
 
        protected override void ApplyDocumentRemoved(DocumentId documentId)
        {
            Debug.Assert(_applyChangesProjectFile != null);
 
            var document = this.CurrentSolution.GetDocument(documentId);
            if (document?.FilePath is not null)
            {
                _applyChangesProjectFile.RemoveDocumentAsync(document.FilePath, CancellationToken.None).Wait();
                this.DeleteDocumentFile(document.Id, document.FilePath);
                this.OnDocumentRemoved(documentId);
            }
        }
 
        private void DeleteDocumentFile(DocumentId documentId, string fullPath)
        {
            try
            {
                if (File.Exists(fullPath))
                {
                    File.Delete(fullPath);
                }
            }
            catch (IOException exception)
            {
                _reporter.Report(new DocumentDiagnostic(WorkspaceDiagnosticKind.Failure, exception.Message, documentId));
            }
            catch (NotSupportedException exception)
            {
                _reporter.Report(new DocumentDiagnostic(WorkspaceDiagnosticKind.Failure, exception.Message, documentId));
            }
            catch (UnauthorizedAccessException exception)
            {
                _reporter.Report(new DocumentDiagnostic(WorkspaceDiagnosticKind.Failure, exception.Message, documentId));
            }
        }
 
        private static bool IsInGAC(string filePath)
        {
            return GlobalAssemblyCacheLocation.RootLocations.Any(static (gloc, filePath) => PathUtilities.IsChildPath(gloc, filePath), filePath);
        }
 
        private static string? s_frameworkRoot;
        private static string FrameworkRoot
        {
            get
            {
                if (RoslynString.IsNullOrEmpty(s_frameworkRoot))
                {
                    var runtimeDir = System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory();
                    s_frameworkRoot = Path.GetDirectoryName(runtimeDir); // back out one directory level to be root path of all framework versions
                }
 
                return s_frameworkRoot ?? throw new InvalidOperationException($"Unable to get {nameof(FrameworkRoot)}");
            }
        }
 
        private static bool IsFrameworkReferenceAssembly(string filePath)
        {
            return PathUtilities.IsChildPath(FrameworkRoot, filePath);
        }
 
        protected override void ApplyMetadataReferenceAdded(ProjectId projectId, MetadataReference metadataReference)
        {
            RoslynDebug.AssertNotNull(_applyChangesProjectFile);
            var identity = GetAssemblyIdentity(projectId, metadataReference);
            if (identity is null)
            {
                var message = string.Format(WorkspaceMSBuildResources.Unable_to_add_metadata_reference_0, metadataReference.Display);
                _reporter.Report(new ProjectDiagnostic(WorkspaceDiagnosticKind.Failure, message, projectId));
                return;
            }
 
            if (metadataReference is PortableExecutableReference peRef && peRef.FilePath != null)
            {
                if (IsInGAC(peRef.FilePath) && identity != null)
                {
                    // Since the location of the reference is in GAC, need to use full identity name to find it again.
                    // This typically happens when you base the reference off of a reflection assembly location.
                    _applyChangesProjectFile.AddMetadataReferenceAsync(identity.GetDisplayName(), metadataReference.Properties.Aliases, hintPath: null, CancellationToken.None).Wait();
                }
                else if (IsFrameworkReferenceAssembly(peRef.FilePath))
                {
                    // just use short name since this will be resolved by msbuild relative to the known framework reference assemblies.
                    var fileName = identity != null ? identity.Name : Path.GetFileNameWithoutExtension(peRef.FilePath);
                    _applyChangesProjectFile.AddMetadataReferenceAsync(fileName, metadataReference.Properties.Aliases, hintPath: null, CancellationToken.None).Wait();
                }
                else // other location -- need hint to find correct assembly
                {
                    var relativePath = PathUtilities.GetRelativePath(Path.GetDirectoryName(CurrentSolution.GetRequiredProject(projectId).FilePath)!, peRef.FilePath);
                    var fileName = Path.GetFileNameWithoutExtension(peRef.FilePath);
                    _applyChangesProjectFile.AddMetadataReferenceAsync(fileName, metadataReference.Properties.Aliases, relativePath, CancellationToken.None).Wait();
                }
            }
 
            this.OnMetadataReferenceAdded(projectId, metadataReference);
        }
 
        protected override void ApplyMetadataReferenceRemoved(ProjectId projectId, MetadataReference metadataReference)
        {
            RoslynDebug.AssertNotNull(_applyChangesProjectFile);
            var identity = GetAssemblyIdentity(projectId, metadataReference);
            if (identity is null)
            {
                var message = string.Format(WorkspaceMSBuildResources.Unable_to_remove_metadata_reference_0, metadataReference.Display);
                _reporter.Report(new ProjectDiagnostic(WorkspaceDiagnosticKind.Failure, message, projectId));
                return;
            }
 
            if (metadataReference is PortableExecutableReference peRef && peRef.FilePath != null)
            {
                _applyChangesProjectFile.RemoveMetadataReferenceAsync(identity.Name, identity.GetDisplayName(), peRef.FilePath, CancellationToken.None).Wait();
            }
 
            this.OnMetadataReferenceRemoved(projectId, metadataReference);
        }
 
        private AssemblyIdentity? GetAssemblyIdentity(ProjectId projectId, MetadataReference metadataReference)
        {
            var project = this.CurrentSolution.GetProject(projectId);
            if (project is null)
            {
                return null;
            }
 
            if (!project.MetadataReferences.Contains(metadataReference))
            {
                project = project.AddMetadataReference(metadataReference);
            }
 
            var compilation = project.GetCompilationAsync(CancellationToken.None).WaitAndGetResult_CanCallOnBackground(CancellationToken.None);
            if (compilation is null)
            {
                return null;
            }
 
            var symbol = compilation.GetAssemblyOrModuleSymbol(metadataReference) as IAssemblySymbol;
            return symbol?.Identity;
        }
 
        protected override void ApplyProjectReferenceAdded(ProjectId projectId, ProjectReference projectReference)
        {
            Debug.Assert(_applyChangesProjectFile != null);
 
            var project = this.CurrentSolution.GetProject(projectReference.ProjectId);
            if (project?.FilePath is not null)
            {
                // Only "ReferenceOutputAssembly=true" project references are represented in the workspace:
                var reference = new ProjectFileReference(project.FilePath, projectReference.Aliases, referenceOutputAssembly: true);
                _applyChangesProjectFile.AddProjectReferenceAsync(project.Name, reference, CancellationToken.None).Wait();
            }
 
            this.OnProjectReferenceAdded(projectId, projectReference);
        }
 
        protected override void ApplyProjectReferenceRemoved(ProjectId projectId, ProjectReference projectReference)
        {
            Debug.Assert(_applyChangesProjectFile != null);
 
            var project = this.CurrentSolution.GetProject(projectReference.ProjectId);
            if (project?.FilePath is not null)
            {
                _applyChangesProjectFile.RemoveProjectReferenceAsync(project.Name, project.FilePath, CancellationToken.None).Wait();
            }
 
            this.OnProjectReferenceRemoved(projectId, projectReference);
        }
 
        protected override void ApplyAnalyzerReferenceAdded(ProjectId projectId, AnalyzerReference analyzerReference)
        {
            Debug.Assert(_applyChangesProjectFile != null);
 
            if (analyzerReference is AnalyzerFileReference fileRef)
            {
                _applyChangesProjectFile.AddAnalyzerReferenceAsync(fileRef.FullPath, CancellationToken.None).Wait();
            }
 
            this.OnAnalyzerReferenceAdded(projectId, analyzerReference);
        }
 
        protected override void ApplyAnalyzerReferenceRemoved(ProjectId projectId, AnalyzerReference analyzerReference)
        {
            Debug.Assert(_applyChangesProjectFile != null);
 
            if (analyzerReference is AnalyzerFileReference fileRef)
            {
                _applyChangesProjectFile.RemoveAnalyzerReferenceAsync(fileRef.FullPath, CancellationToken.None).Wait();
            }
 
            this.OnAnalyzerReferenceRemoved(projectId, analyzerReference);
        }
    }
 
    #endregion
}