dotnet / project-system

The .NET Project System for Visual Studio
MIT License
967 stars 385 forks source link

Roslyn symbol resolution fails after unloading and re-loading an SDK-style project. #4794

Open kfertitta opened 5 years ago

kfertitta commented 5 years ago

In VS2017 15.9.11, I use the shell interfaces to programmatically unload and reload a C# project, and after the reload, Roslyn symbol resolution does not work. Inserting a delay (of up to 60 seconds) doesn't help.

Interestingly, this behavior is NOT observed with a non-SDK-style project. After unloading and re-loading the project, Roslyn is able to immediately resolve symbols.

The following project is all that is needed to see the failure:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net45</TargetFramework>
  </PropertyGroup>

</Project>

Use this code to resolve/unload/reload/resolve again:

var componentModel = (IComponentModel)serviceProvider.GetService(typeof(SComponentModel));

var workspace = componentModel.GetService<VisualStudioWorkspace>();

var project = workspace.CurrentSolution.Projects.Single();

var compilation = ThreadHelper.JoinableTaskFactory.Run(async () => await project.GetCompilationAsync());

// Symbol resolves here.
//
var symbol = compilation.GetSpecialType(SpecialType.System_Int32);

var solution = (IVsSolution)serviceProvider.GetService(typeof(SVsSolution));

solution.GetProjectOfUniqueName(project.FilePath, out var hierarchy);

var projectId = solution.GetProjectIdGuid(hierarchy);

solution.UnloadProject(hierarchy);

solution.ReloadProject(projectId);

project = workspace.CurrentSolution.Projects.Single();

compilation = ThreadHelper.JoinableTaskFactory.Run(async () => await project.GetCompilationAsync());

// This returns null.
//
var symbol = compilation.GetSpecialType(SpecialType.System_Int32);

If you repeat the above on a from-scratch C# application that is NOT SDK-style, then you will see the above symbol resolution work just fine -- before and after the re-load.

It "feels" like Roslyn is waiting for something to "kick" it after an unload/reload cycle.

Very interested in this because we have a critical need to be able to unload a project do some work on the project file, reload it, and do some Roslyn transforms.

Thanks very much in advance.

jjmew commented 5 years ago

@kfertitta In the SDK-Style projects there is not a need to unload to modify the project file. This was one of the goals when we implement it. If you find that there is a need to reload please let us know so we can fix that specific issue.

kfertitta commented 5 years ago

Hi @jjmew,

Thank you for the prompt reply. However, the fact that unloading it programmatically breaks Roslyn is problematic.

Let me just give some more insights into our use case to clarify. The above repro was just a minimal repro developed to help highlight the issue. We have a command in our VSIX extension that "transforms" a simple non-SDK-style C# class library project into an SDK-style project. It does this by unloading the initial (non-SDK-style) non-SDK-style project using the above techniques. We transform a few MSBuild elements directly and re-load. Very simple. After the reload completes, the SDK-style project is not fully realized in Roslyn at all. The Roslyn Project shows there are no documents. You cannot resolve the symbol for an int (as per the above), etc. Inspecting the state of the project at that point via either CPS or the classic COM-based shell APIs confirm that the project is loaded and all source items are present.

If you totally exit VS and re-open that same transformed SDK-style project, then all is well -- the documents appear and symbol resolution works fine.

Does this help clarify?

Thanks very much again and looking forward to your reply.

jjmew commented 5 years ago

@kfertitta Thank you for the information. @davidwengier will take a look

kfertitta commented 5 years ago

@jjmew Thanks very much for that. I appreciate it.

davidwengier commented 5 years ago

Unfortunately this mix of concepts (Roslyn and DTE) is not well supported and is difficult to work with. A long enough wait after reloading should work, as the DTE API is synchronous but doesn't block to wait for results. Sadly there is no way to tell when is "long enough", but the code itself seems okay. On my machine your pseudocode works with only the barest minimum tweaks to get it compiling, in situations where a design time build isn't needed.

You mention that you've tried using CPS to do this, and I suspect that approach will net better results. If you can post the code that you're using for that approach we might be able to help with it, or you could also get help on their Gitter channel, or on https://github.com/microsoft/vsprojectsystem

kfertitta commented 5 years ago

@davidwengier Thank you very much for the reply and the insights. In terms of Gitter, the vsprojectsystem one is "deprecated" and the "cool" kids are hanging out on "extendvs". ;-)

I'm on there a lot, so if I should post there, I certainly will.

Just one clarification, in regards to my reference to CPS. I have actually not tried using the CPS APIs to do the loading/unloading. I only used it to check the status of the reloaded project -- interrogate the source items, etc -- just to validate that the project system view of the world was fully realized, in contrast to what Roslyn was seeing.

Now I'm surprised that the code above works for you. Did you do it in VS2017? I'm working exclusively there because our product must support it for quite some time.

davidwengier commented 5 years ago

The exact code I tried, in Visual Studio 2019, is the following:

var serviceProvider = Microsoft.VisualStudio.Shell.ServiceProvider.GlobalProvider;

            var componentModel = (IComponentModel)serviceProvider.GetService(typeof(SComponentModel));

            var workspace = componentModel.GetService<VisualStudioWorkspace>();

            var project = workspace.CurrentSolution.Projects.Single();

            var compilation = ThreadHelper.JoinableTaskFactory.Run(async () => await project.GetCompilationAsync());

            // Symbol resolves here.
            //
            var symbol = compilation.GetSpecialType(SpecialType.System_Int32);

            var solution = (IVsSolution)serviceProvider.GetService(typeof(SVsSolution));
            var solution4 = (IVsSolution4)solution;

            solution.GetProjectOfUniqueName(project.FilePath, out var hierarchy);

            solution.GetGuidOfProject(hierarchy, out Guid projectId);

            solution4.UnloadProject(ref projectId, (uint)_VSProjectUnloadStatus.UNLOADSTATUS_UnloadedByUser);

            solution4.ReloadProject(projectId);

            project = workspace.CurrentSolution.Projects.Single();

            compilation = ThreadHelper.JoinableTaskFactory.Run(async () => await project.GetCompilationAsync());

            // This DOESN'T return null.
            //
             symbol = compilation.GetSpecialType(SpecialType.System_Int32);

As I mentioned though, since this code makes no changes between Unload and Reload, there would presumably be no design time build required, and timing is presumably not an issue.

kfertitta commented 5 years ago

@davidwengier

I can also confirm that using either my original code or your slightly modified version that it works in VS2019. I didn't have a VM setup to test that VS2019 at the time. But, VS2017 is a hard requirement for us, so I'm looking for some kind of workaround. It's 100% repeatable in both VS versions -- always fails in VS2017 and always succeeds in VS2019.

I spent a fair bit of time experimenting with inserting delays and even executing empty custom targets after the re-load, to see if that would trigger Roslyn to do its thing. But, none of it made a difference. I can see in the debugger by assigning Object IDs to the Project and CurrentSolution before and after the re-load that both are different, but the project just has no contents. Maybe there is a design-time target in Microsoft.Common.targets that would trigger what we need?

We're sorta stymied without something workable in VS2017. Doesn't have to be perfect, but just repeatable and reliable.

Any insights here are very much appreciated.

jmarolf commented 5 years ago

In general, this is a problem that I don't think is resolvable on VS 2017.

This is the bug we are using to track this: https://github.com/dotnet/project-system/issues/3425. Basically we need an API that allows you to know when both roslyn and the project system (two asynchronous services) are in sync with each other. We plan to have a solution for this in a future update to VS 2019.

A workaround I can suggest (though it is not perfect) is to listen for workspace-changed events from roslyn. The project system will create many workspace-changed events on project reload. Once that is done roslyn will have been "populated" with the project state.

kfertitta commented 5 years ago

@jmarolf Thank you for taking the time to reply. I will try the workspace-changed event approach and report back. I do, in fact, see that 16.1 preview 3 seems to resolve the issue I'm seeing. Not sure if the full surface area of Roslyn/project system synchronization is resolved with that build, but this repro consistently succeeds there.

Thanks again and I'll report back shortly.

jmarolf commented 5 years ago

@kfertitta to be clear, our goal is to ensure that anything that you could do in the older project system still works in the new one. The fundamental asynchronicity of all these components makes it harder and I suspect there are still some subtle bugs in things working even in 16.1. So please report any issues you find where APIs don't work as expected. The plan is to fix all these things

kfertitta commented 5 years ago

@jmarolf Understood. I absolutely will. Very much appreciate the team's responsiveness here. These are critical scenarios for us, so we're willing to invest whatever it takes.

My business partner, Rob Mensching (of WiX fame), is onsite at the Visual Studio Partner event tomorrow to discuss some of our scenarios and such. We have a big release on Monday so I'm having to cancel last minute.

kfertitta commented 5 years ago

@jmarolf and @davidwengier

An update here. So far, I've managed only a partial workaround -- I'm able to get trees with the following which leverages the TPL source blocks. The project is not fully hydrated, as resolving symbols (even System.Int32) fails.

IDisposable link = null;

link = unconfiguredProject.Services.ActiveConfiguredProjectSubscription.ProjectRuleSource.SourceBlock
    .LinkTo(
        new ActionBlock<IProjectVersionedValue<IProjectSubscriptionUpdate>>(u => OnProjectChanged(u)),
        new DataflowLinkOptions { PropagateCompletion = true });

void OnProjectChanged(IProjectVersionedValue<IProjectSubscriptionUpdate> update)
{
    // Unsubscribe right away, as we only want to do this once.
    //
    link?.Dispose();

    // Go get trees, but not symbols...  :-(
}