diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/LanguageServices/Workspace.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/LanguageServices/Workspace.cs index 2b82580b47..81aa28a71c 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/LanguageServices/Workspace.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/LanguageServices/Workspace.cs @@ -334,7 +334,7 @@ void ProcessProjectEvaluationHandlers() // for example from Debug to Release. ConfiguredProject configuredProject = update.Value.ConfiguredProject; - IComparable version = GetConfiguredProjectVersion(update); + IComparable version = GetVersion(update); foreach (IProjectEvaluationHandler evaluationHandler in _updateHandlers.EvaluationHandlers) { @@ -448,7 +448,7 @@ void InvokeCommandLineUpdateHandlers() Assumes.Present(parser); - IComparable version = GetConfiguredProjectVersion(update); + IComparable version = GetVersion(update); string baseDirectory = _unconfiguredProject.GetProjectDirectory(); @@ -543,9 +543,9 @@ void UpdateProgressRegistration() } } - private static IComparable GetConfiguredProjectVersion(IProjectValueVersions update) + private static IComparable GetVersion(IProjectValueVersions update) { - return update.DataSourceVersions[ProjectDataSources.ConfiguredProjectVersion]; + return new CompositeVersion(update.DataSourceVersions); } public async Task WriteAsync(Func action, CancellationToken cancellationToken) @@ -599,6 +599,46 @@ internal void Fault(Exception exception) _contextCreated.TrySetException(exception); } + /// + /// Combines both the configured project version and the active configured project version + /// into a single comparable value. In this way, we can ensure we order updates correctly, + /// even when the active configuration changes. + /// + private sealed class CompositeVersion(IImmutableDictionary versions) : IComparable + { + private readonly IComparable _configuredProjectVersion = versions[ProjectDataSources.ConfiguredProjectVersion]; + private readonly IComparable _activeConfigurationVersion = versions[ProjectDataSources.ActiveProjectConfiguration]; + + public int CompareTo(object obj) + { + if (obj is not CompositeVersion other) + { + throw new ArgumentException("Cannot compare to a non-CompositeVersion object."); + } + + // The active configuration version will only increase. Every time the configuration + // changes, this value goes up. + // + // We check this *before* checking the configured project version, because when + // switching active configuration, the configured project version can go *down*. + // However, it's important for our processing of evaluation and build data that + // the version is monotonic (only increases). + // + // We achieve monotonicity by combining these two versions. + int c = _activeConfigurationVersion.CompareTo(other._activeConfigurationVersion); + + if (c is not 0) + { + // Active configuration differs, so compare on this alone. + return c; + } + + // Active configuration is identical. Compare the configured project versions + // for that configuration directly. + return _configuredProjectVersion.CompareTo(other._configuredProjectVersion); + } + } + /// /// Maps from our property data snapshot to Roslyn's API for accessing the project's /// evaluation data. diff --git a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/LanguageServices/WorkspaceTests.cs b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/LanguageServices/WorkspaceTests.cs index abd11cb366..06dce7d9ba 100644 --- a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/LanguageServices/WorkspaceTests.cs +++ b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/LanguageServices/WorkspaceTests.cs @@ -77,7 +77,7 @@ public async Task Dispose_DisposesHandlers() factory: () => Mock.Of(MockBehavior.Loose), disposeAction: () => disposeCount++); - var workspace = await CreateInstanceAsync(updateHandlers: new UpdateHandlers(new[] { exportFactory })); + var workspace = await CreateInstanceAsync(updateHandlers: new UpdateHandlers([exportFactory])); Assert.Equal(0, disposeCount); @@ -382,7 +382,7 @@ public async Task EvaluationUpdate_InvokesEvaluationHandlersWhenChangesExist(boo o => o.Handle( workspaceProjectContext.Object, It.IsAny(), - 1, + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); @@ -391,7 +391,7 @@ public async Task EvaluationUpdate_InvokesEvaluationHandlersWhenChangesExist(boo var workspace = await CreateInstanceAsync( evaluationRuleUpdate: evaluationRuleUpdate, workspaceProjectContext: workspaceProjectContext.Object, - updateHandlers: new UpdateHandlers(new[] { ExportFactoryFactory.Implement(() => updateHandler.Object) })); + updateHandlers: new UpdateHandlers([ExportFactoryFactory.Implement(() => updateHandler.Object)])); updateHandler.Verify(); } @@ -443,7 +443,7 @@ public async Task EvaluationUpdate_InvokesProjectEvaluationHandlersWhenChangesEx o => o.Handle( workspaceProjectContext.Object, It.IsAny(), - 1, + It.IsAny(), It.IsAny(), new ContextState(false, true), It.IsAny())); @@ -452,7 +452,7 @@ public async Task EvaluationUpdate_InvokesProjectEvaluationHandlersWhenChangesEx var workspace = await CreateInstanceAsync( evaluationRuleUpdate: evaluationRuleUpdate, workspaceProjectContext: workspaceProjectContext.Object, - updateHandlers: new UpdateHandlers(new[] { ExportFactoryFactory.Implement(() => updateHandler.Object) })); + updateHandlers: new UpdateHandlers([ExportFactoryFactory.Implement(() => updateHandler.Object)])); updateHandler.Verify(); } @@ -497,7 +497,7 @@ public async Task EvaluationUpdate_InvokesSourceItemHandlersWhenChangesExist(boo var workspace = await CreateInstanceAsync( sourceItemsUpdate: sourceItemsUpdate, workspaceProjectContext: workspaceProjectContext.Object, - updateHandlers: new UpdateHandlers(new[] { ExportFactoryFactory.Implement(() => updateHandler.Object) })); + updateHandlers: new UpdateHandlers([ExportFactoryFactory.Implement(() => updateHandler.Object)])); updateHandler.Verify(); } @@ -546,7 +546,7 @@ public async Task BuildUpdate_InvokesCommandLineHandlerWhenChangesExist(bool any commandLineHandler.Setup( o => o.Handle( workspaceProjectContext.Object, - 1, + It.IsAny(), It.Is(options => options.MetadataReferences.Select(r => r.Reference).SingleOrDefault() == "Added.dll"), It.Is(options => options.MetadataReferences.Select(r => r.Reference).SingleOrDefault() == "Removed.dll"), new ContextState(false, true), @@ -567,7 +567,7 @@ public async Task BuildUpdate_InvokesCommandLineHandlerWhenChangesExist(bool any buildRuleUpdate: buildRuleUpdate, commandLineParserServices: commandLineParserServices, workspaceProjectContext: workspaceProjectContext.Object, - updateHandlers: new UpdateHandlers(new[] { ExportFactoryFactory.Implement(() => updateHandler.Object) })); + updateHandlers: new UpdateHandlers([ExportFactoryFactory.Implement(() => updateHandler.Object)])); updateHandler.Verify(); } @@ -864,7 +864,8 @@ private static async Task ApplyEvaluationAsync( ConfiguredProject? configuredProject = null, IProjectSubscriptionUpdate? evaluationRuleUpdate = null, IProjectSubscriptionUpdate? sourceItemsUpdate = null, - int configuredProjectVersion = 1) + int configuredProjectVersion = 1, + int activeConfigurationVersion = 1) { configuredProject ??= ConfiguredProjectFactory.Create(); @@ -912,14 +913,20 @@ private static async Task ApplyEvaluationAsync( var update = WorkspaceUpdate.FromEvaluation((configuredProject, projectSnapshot, evaluationRuleUpdate, sourceItemsUpdate)); await workspace.OnWorkspaceUpdateAsync( - IProjectVersionedValueFactory.Create(update, ProjectDataSources.ConfiguredProjectVersion, configuredProjectVersion)); + IProjectVersionedValueFactory.Create( + update, + dataSourceVersions: ImmutableDictionary.CreateRange([ + new(ProjectDataSources.ConfiguredProjectVersion, configuredProjectVersion), + new(ProjectDataSources.ActiveProjectConfiguration, activeConfigurationVersion) + ]))); } private static async Task ApplyBuildAsync( Workspace workspace, ConfiguredProject? configuredProject = null, IProjectSubscriptionUpdate? buildRuleUpdate = null, - int configuredProjectVersion = 1) + int configuredProjectVersion = 1, + int activeConfigurationVersion = 1) { configuredProject ??= ConfiguredProjectFactory.Create(); @@ -944,6 +951,11 @@ private static async Task ApplyBuildAsync( var update = WorkspaceUpdate.FromBuild((configuredProject, buildRuleUpdate)); await workspace.OnWorkspaceUpdateAsync( - IProjectVersionedValueFactory.Create(update, ProjectDataSources.ConfiguredProjectVersion, configuredProjectVersion)); + IProjectVersionedValueFactory.Create( + update, + dataSourceVersions: ImmutableDictionary.CreateRange([ + new(ProjectDataSources.ConfiguredProjectVersion, configuredProjectVersion), + new(ProjectDataSources.ActiveProjectConfiguration, activeConfigurationVersion) + ]))); } }