Support multi-targeting for Roslyn components
See original GitHub issueBackground
As Roslyn implements more features and APIs, the need to target these features/APIs increases in Roslyn components (i.e. Code Analyzers/Fixers and Source Generators). As the components need to target newer versions of Roslyn, it is becoming clear that some components need to target multiple versions of Roslyn at the same time.
Take, for example, the following issues in dotnet/runtime with the System.Text.Json source generator:
In both of these issues, the only fix is for the System.Text.Json source generator to take a dependency on version 4.0
of the Microsoft.CodeAnalysis
assemblies. The first needs IIncrementalGenerator
and the second needs FileScopedNamespaceDeclarationSyntax
, both of which were only introduced in 4.0
.
However, once a Roslyn component references a version of Microsoft.CodeAnalysis
, that Roslyn component can no longer be loaded in an earlier version of the compiler.
This poses two problems:
- If a Roslyn component still wants to work in a previous version of the compiler, for example in Visual Studio 2019, the component has a problem. It needs to pick which version it targets - either older versions without the new API support, or the new version.
- If a Roslyn component chooses to target the new version, there is no way to shut off the component when loaded in older versions. Thus, when a developer in VS 2019 references a NuGet package with a component targeting Roslyn
4.0
, the developer gets a warning in their build:
Warning CS8032 An instance of analyzer System.Text.Json.SourceGeneration.JsonSourceGenerator cannot be created from C:\Users\eerhardt\.nuget\packages\system.text.json\7.0.0-dev\analyzers\dotnet\cs\System.Text.Json.SourceGeneration.dll : Could not load file or assembly 'Microsoft.CodeAnalysis, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' or one of its dependencies. The system cannot find the file specified.
This is not a great experience for our customers.
Proposal
We will update the current logic that resolves the Analyzers from NuGet packages to support an enhanced analyzers
folder structure. The enhanced folder structure will allow for the Roslyn version targeted by the component to be expressed. This will allow for multiple versions to be targeted in a single package, as well as being able to express a “minimal” version. This will address the two problems above. When the Analyzers are resolved from the NuGet package, the correct asset(s) will be resolved according to which version of the compiler will be used to compile the assembly.
Current folder structure
The following structure is recognized today by the SDK:
analyzers[\dotnet][\cs|vb]\component.dll
Proposed folder structure
This proposal adds a new, optional folder to the structure: roslyn{version}
:
analyzers[\dotnet][\roslyn{version}][\cs|vb]\component.dll
Where {version}
is the {major}.{minor}
version of the Microsoft.CodeAnalysis
assemblies used during the Compile
task.
When a NuGet package contains folders under analyzers
with the pattern, roslyn<major-version>.<minor-version>
, the folder with the highest version that is less than or equal to the current Microsoft.CodeAnalysis
<major>.<minor>
version will be used. This works the same as TFMs work under the lib
folder. Except instead of TargetFramework versions being evaluated, the Roslyn version will be evaluated.
Also note, since the same Analyzer logic exists in NuGet’s ResolveNuGetPackageAssets Task. Thus, we will need to update the NuGet MSBuild logic with this support. That way developers using non-SDK .NET projects will get the same Analyzers selected as SDK projects.
Earlier SDK versions
Since this proposal is to add support to .NET SDK 6.0, we need to consider what happens when NuGet packages that follow this structure are used in SDK versions before 6.0.
If implemented as indicated above, NuGet packages that support multiple Roslyn versions will have all of their assets loaded in earlier versions of the SDK. To prevent that, we can add a new MSBuild property to the SDK that indicates it supports multi-targeting Roslyn versions.
$(SupportsRoslynComponentVersioning)
NuGet packages can include MSBuild logic that “lights up” when $(SupportsRoslynComponentVersioning)
is not present. This indicates that the built-in support isn’t there to select the right version. The NuGet package needs to do the selection itself. Typically, NuGet packages will select the component that targets the lowest version of Roslyn version it supports, since newer Roslyn versions can’t be guaranteed. Another option would be to opt out of adding a Roslyn component all together.
Additional rules
- When a package contains analyzer assets both inside a
roslyn{version}
folder and outside, ex. directly underanalyzers\dotnet\cs\analyzer.dll
, both assets will be selected. The reasoning is that the assets outside ofroslyn{version}
folders are considered to work everywhere. Existing analyzer resolution rules add both assets inside and outside{language}
folders, if present. This follows the same reasoning.- If a NuGet package wants to express a component that should be used when none of the
roslyn{version}
folders apply, it can add that asset toroslyn1.0
.
- If a NuGet package wants to express a component that should be used when none of the
Alternative Solutions
An alternative is to follow the proposal in https://github.com/dotnet/roslyn/issues/54108:
The current plan is to provide authoring and consumption targets that make this easy to do correctly. Over time we’ll aim to get NuGet itself updated to support the feature in-box and remove the need for the targets.
Following this approach would mean that each NuGet package that wants to support multiple versions of Roslyn (or a minimum version) would need to ship special MSBuild .targets
files with duplicated logic in them.
This approach is not ideal because:
- If there is a bug in the targets, it needs to be fixed in all NuGet packages that copied the targets file.
- There could be potential conflicts between the targets in the NuGet packages if they are not named uniquely.
- The logic to resolve the Roslyn version shouldn’t be the responsibility of the NuGet packages that ship Roslyn components.
Issue Analytics
- State:
- Created 2 years ago
- Reactions:2
- Comments:18 (15 by maintainers)
Top GitHub Comments
@jaredpar
We spoke about this pretty early on in the design that you, me and @jmarolf talked about. There are I think a couple of compelling reasons not to use the SDK version:
I think given the fairly limited expected audience that are going to do this, along with the fact that they really want to reason about roslyn itself, we should use the API version here. In the future if we want to expand it out to more users we can go the SDK / targets path where we still use it but as an implementation detail hidden from the user.
To me, this isn’t a reasonable approach because System.Text.Json also ships as a NuGet package. This allows developers to use the new functionality without forcing them to move to .NET 6. That’s the whole reason we ship it in a NuGet package and support older TFMs. Imagine the scenario where you have a .NET Framework application that uses System.Text.Json v5.0 NuGet package. We ship the new STJ v6.0 NuGet package. Visual Studio is going to lead you to update your NuGet package references to the latest versions (there is an “Updates” section in the NuGet package manager UI). If you click that button, you now get a warning in your build that “the System.Text.Json source generator could not be loaded”.
There is a big difference between targeting .NET 6 and updating NuGet package dependencies. It’s reasonable to say “If you want to target .NET 6, use VS 2022”. I don’t think it is reasonable to say “if you want to update this NuGet package version, you need to also update your VS”.
Agreed that it doesn’t fully solve multi-targeting between VS 2019 and VS 2022. We will still need to ship custom logic in our NuGet packages. But that custom logic can be much simpler if this feature is in the .NET 6 SDK.
If this feature is in the .NET 6 SDK, the custom NuGet logic would logically look like:
Because if the NuGet package is being used in an environment that doesn’t have
$(SupportsRoslynComponentVersioning)
, we can just use the component that works with the oldest Roslyn.But if this feature isn’t in the .NET 6 SDK, the custom NuGet logic needs to get much more complicated. It basically needs to do the multi-targeting logic itself: figure out the Roslyn version, select the most appropriate analyzer assembly, etc.
I’m not familiar with “an incomplete version of C# 10 is shipping in VS 16.11”, but my understanding is that the Microsoft.CodeAnalysis version in VS 16.11 will always be
3.11.*
. This proposal is for the Roslyn components to be selected based on which version of Microsoft.CodeAnalysis they reference. Not based on the LangVersion.I agree that this isn’t the ideal long-term location for this logic, but this is the place that is doing the language (
cs
vs.vb
) selection logic. Given that logic lives here, if we were to implement this proposal, it makes the most sense to put this new multi-target logic in the same place as the language selection logic.I’m not sure I’m convinced of that myself. Having NuGet need to understand all of the conventions used in NuGet packages is a limiting design IMO. We have proposals all the time for new conventions, and forcing NuGet to understand all of them forces too much burden in the package manager.
(aside: extensions to .NET Interactive are loaded based on a convention based location in the NuGet package. I don’t think NuGet should need to understand them.)
IMO, the ideal long-term location for this logic would be in the Compiler/Roslyn targets. Roslyn owns the scenario of why people put
/analyzers
in their NuGet packages. Roslyn should be the one that defines the conventions used in the NuGet package.dotnet/runtime
has 2 source generators that both would use this functionality: System.Text.Json and Microsoft.Extensions.Logging.There is also https://github.com/reactiveui/refit/pull/1216 which has already done its own multi-targeting for the same reasons as the
dotnet/runtime
generators.