Skip to content

Commit 8eb9df9

Browse files
committed
Fix Vsix Microsoft.Bcl.AsyncInterfaces version conflict with VS 17.x
VS 17.14 ships Microsoft.Bcl.AsyncInterfaces 9.0.0.0 and devenv.exe.config binding-redirects 0-9.0.0.0 to that version. Anything in the Vsix that references >9.0.0.0 falls outside the redirect, so devenv loads a second copy from the extension folder, producing duplicate IAsyncDisposable type identities and silent runtime failures. CodeConverter was transitively pulling in Microsoft.Bcl.AsyncInterfaces 10.0.0.0 via System.Linq.AsyncEnumerable (which only ships as a 10.x package) and System.Text.Json 10.0.0. Drop those package references from CodeConverter and hand-roll the small subset of System.Linq.AsyncEnumerable we actually used (ToArraySafeAsync, AsAsyncEnumerable, SelectSafe). The helpers are deliberately renamed so they do not clash with .NET 10's BCL System.Linq.AsyncEnumerable extension methods when referenced by a multi-targeted consumer. System.Linq.AsyncEnumerable is added directly to CodeConv.csproj instead - the CLI tool never loads into devenv.exe so the binding-redirect ceiling does not apply there. Add VsixAssemblyCompatibilityTests that statically walks the Vsix output via PEReader and asserts every referenced version of a known VS-owned polyfill (currently Microsoft.Bcl.AsyncInterfaces) is satisfiable by the version the oldest supported VS ships. The test reproduces the original bug and now passes.
1 parent e596706 commit 8eb9df9

File tree

10 files changed

+314
-19
lines changed

10 files changed

+314
-19
lines changed

CodeConv/CodeConv.csproj

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@
3131

3232
<ItemGroup>
3333
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.2" />
34+
<!--
35+
System.Linq.AsyncEnumerable only ships as a 10.x package, so it transitively pulls in
36+
Microsoft.Bcl.AsyncInterfaces 10.0.0.0. That's fine here because the CodeConv CLI never
37+
loads into devenv.exe (unlike the Vsix), so the VS-shipped binding redirect ceiling on
38+
Microsoft.Bcl.AsyncInterfaces does not apply. The CodeConverter library itself avoids
39+
depending on this package precisely so the Vsix output stays compatible with VS 17.x.
40+
See VsixAssemblyCompatibilityTests.
41+
-->
42+
<PackageReference Include="System.Linq.AsyncEnumerable" Version="10.0.0" />
3443
<PackageReference Include="Microsoft.Build.Tasks.Core" Version="17.8.43" />
3544
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0" />
3645
<PackageReference Include="Microsoft.CodeAnalysis.VisualBasic.Workspaces" Version="4.14.0" />

CodeConverter/CSharp/HandledEventsAnalyzer.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,10 @@ private async Task<HandledEventsAnalysis> AnalyzeAsync()
3737
#pragma warning restore RS1024 // Compare symbols correctly
3838

3939

40-
var writtenWithEventsProperties = await ancestorPropsMembersByName.Values.OfType<IPropertySymbol>().ToAsyncEnumerable()
41-
.ToDictionaryAsync(async (p, _) => p.Name, async (p, cancellationToken) => (p, await IsNeverWrittenOrOverriddenAsync(p, cancellationToken)));
40+
var writtenWithEventsProperties = new Dictionary<string, (IPropertySymbol p, bool)>();
41+
foreach (var prop in ancestorPropsMembersByName.Values.OfType<IPropertySymbol>()) {
42+
writtenWithEventsProperties[prop.Name] = (prop, await IsNeverWrittenOrOverriddenAsync(prop));
43+
}
4244

4345
var eventContainerToMethods = _type.GetMembers().OfType<IMethodSymbol>()
4446
.SelectMany(HandledEvents)

CodeConverter/CSharp/ProjectMergedDeclarationExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public static async Task<Project> WithRenamedMergedMyNamespaceAsync(this Project
6363
var projectDir = Path.Combine(vbProject.GetDirectoryPath(), "My Project");
6464

6565
var compilation = await vbProject.GetCompilationAsync(cancellationToken);
66-
var embeddedSourceTexts = await GetAllEmbeddedSourceTextAsync(compilation).Select((r, i) => (Text: r, Suffix: $".Static.{i+1}")).ToArrayAsync(cancellationToken);
66+
var embeddedSourceTexts = await GetAllEmbeddedSourceTextAsync(compilation).SelectSafe((r, i) => (Text: r, Suffix: $".Static.{i+1}")).ToArraySafeAsync(cancellationToken);
6767
var generatedSourceTexts = (Text: await GetDynamicallyGeneratedSourceTextAsync(compilation), Suffix: ".Dynamic").Yield();
6868

6969
foreach (var (text, suffix) in embeddedSourceTexts.Concat(generatedSourceTexts)) {

CodeConverter/CodeConverter.csproj

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,6 @@
5454
<PackageReference Include="System.Data.DataSetExtensions" Version="4.5.0" />
5555
<PackageReference Include="System.Globalization.Extensions" Version="4.3.0" />
5656
<PackageReference Include="System.IO.Abstractions" Version="13.2.33" />
57-
<PackageReference Include="System.Linq.AsyncEnumerable" Version="10.0.0" />
58-
<PackageReference Include="System.Text.Encodings.Web" Version="10.0.0" />
59-
<PackageReference Include="System.Text.Json" Version="10.0.0" />
6057
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="5.0.0">
6158
<IncludeAssets>all</IncludeAssets>
6259
</PackageReference>

CodeConverter/Common/AsyncEnumerableTaskExtensions.cs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,68 @@ public static async Task<TResult[]> SelectAsync<TArg, TResult>(this IEnumerable<
9292

9393
return partitionResults.ToArray();
9494
}
95+
96+
/// <summary>
97+
/// Hand-rolled to avoid depending on <c>System.Linq.AsyncEnumerable</c>, which only ships as a 10.x
98+
/// package and so transitively forces Microsoft.Bcl.AsyncInterfaces 10.0.0.0 into the Vsix output —
99+
/// a version Visual Studio 17.x cannot bind to. See <c>VsixAssemblyCompatibilityTests</c>.
100+
/// </summary>
101+
/// <remarks>
102+
/// Deliberately not named <c>ToArrayAsync</c> so it does not clash with
103+
/// <see cref="System.Linq.AsyncEnumerable"/>.<c>ToArrayAsync</c> on .NET 10+ when this assembly is
104+
/// referenced by a project whose target framework provides the BCL version.
105+
/// </remarks>
106+
public static async Task<TSource[]> ToArraySafeAsync<TSource>(this IAsyncEnumerable<TSource> source,
107+
CancellationToken cancellationToken = default)
108+
{
109+
var list = new List<TSource>();
110+
await foreach (var item in source.WithCancellation(cancellationToken)) {
111+
list.Add(item);
112+
}
113+
return list.ToArray();
114+
}
115+
116+
/// <summary>
117+
/// Adapts a synchronous sequence to <see cref="IAsyncEnumerable{T}"/>. Hand-rolled for the same
118+
/// reason as <see cref="ToArraySafeAsync{TSource}"/>, and likewise renamed to avoid clashing with
119+
/// <see cref="System.Linq.AsyncEnumerable"/>.<c>ToAsyncEnumerable</c> on .NET 10+.
120+
/// </summary>
121+
#pragma warning disable 1998 // async method without await; required for the iterator to compile to IAsyncEnumerable.
122+
#pragma warning disable VSTHRD200 // The method returns IAsyncEnumerable, not a Task; "Async" suffix would be misleading.
123+
public static async IAsyncEnumerable<TSource> AsAsyncEnumerable<TSource>(this IEnumerable<TSource> source)
124+
{
125+
foreach (var item in source) {
126+
yield return item;
127+
}
128+
}
129+
#pragma warning restore 1998
130+
131+
/// <summary>
132+
/// Lazy projection over an <see cref="IAsyncEnumerable{T}"/>. Hand-rolled for the same reason
133+
/// as <see cref="ToArraySafeAsync{TSource}"/>, and renamed to avoid clashing with
134+
/// <see cref="System.Linq.AsyncEnumerable"/>.<c>Select</c> on .NET 10+.
135+
/// </summary>
136+
public static async IAsyncEnumerable<TResult> SelectSafe<TSource, TResult>(this IAsyncEnumerable<TSource> source,
137+
Func<TSource, TResult> selector,
138+
[EnumeratorCancellation] CancellationToken cancellationToken = default)
139+
{
140+
await foreach (var item in source.WithCancellation(cancellationToken)) {
141+
yield return selector(item);
142+
}
143+
}
144+
145+
/// <summary>
146+
/// Lazy projection (with index) over an <see cref="IAsyncEnumerable{T}"/>. Hand-rolled for the same
147+
/// reason as <see cref="ToArraySafeAsync{TSource}"/>.
148+
/// </summary>
149+
public static async IAsyncEnumerable<TResult> SelectSafe<TSource, TResult>(this IAsyncEnumerable<TSource> source,
150+
Func<TSource, int, TResult> selector,
151+
[EnumeratorCancellation] CancellationToken cancellationToken = default)
152+
{
153+
var index = 0;
154+
await foreach (var item in source.WithCancellation(cancellationToken)) {
155+
yield return selector(item, index++);
156+
}
157+
}
158+
#pragma warning restore VSTHRD200
95159
}

CodeConverter/Common/ProjectConversion.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ private ProjectConversion(IProjectContentsConverter projectContentsConverter, IE
5050
if (conversionOptions.SelectedTextSpan is { Length: > 0 } span) {
5151
document = await WithAnnotatedSelectionAsync(document, span);
5252
}
53-
var conversionResults = await ConvertDocumentsAsync<TLanguageConversion>(new[] {document}, conversionOptions, progress, cancellationToken).ToArrayAsync(cancellationToken);
53+
var conversionResults = await ConvertDocumentsAsync<TLanguageConversion>(new[] {document}, conversionOptions, progress, cancellationToken).ToArraySafeAsync(cancellationToken);
5454
var codeResult = conversionResults.First(r => r.SourcePathOrNull == document.FilePath);
5555
codeResult.Exceptions = conversionResults.SelectMany(x => x.Exceptions).ToArray();
5656
return codeResult;
@@ -183,7 +183,7 @@ private async IAsyncEnumerable<ConversionResult> ConvertAsync(IProgress<Conversi
183183
{
184184
var phaseProgress = StartPhase(progress, "Phase 1 of 2:");
185185
var firstPassResults = _documentsToConvert.ParallelSelectAwaitAsync(d => FirstPassLoggedAsync(d, phaseProgress), Env.MaxDop, _cancellationToken);
186-
var (proj1, docs1) = await _projectContentsConverter.GetConvertedProjectAsync(await firstPassResults.ToArrayAsync(_cancellationToken));
186+
var (proj1, docs1) = await _projectContentsConverter.GetConvertedProjectAsync(await firstPassResults.ToArraySafeAsync(_cancellationToken));
187187

188188
var warnings = await GetProjectWarningsAsync(_projectContentsConverter.SourceProject, proj1);
189189
if (!string.IsNullOrWhiteSpace(warnings)) {
@@ -193,7 +193,7 @@ private async IAsyncEnumerable<ConversionResult> ConvertAsync(IProgress<Conversi
193193

194194
phaseProgress = StartPhase(progress, "Phase 2 of 2:");
195195
var secondPassResults = proj1.GetDocuments(docs1).ParallelSelectAwaitAsync(d => SecondPassLoggedAsync(d, phaseProgress), Env.MaxDop, _cancellationToken);
196-
await foreach (var result in secondPassResults.Select(CreateConversionResult).WithCancellation(_cancellationToken)) {
196+
await foreach (var result in secondPassResults.SelectSafe(CreateConversionResult).WithCancellation(_cancellationToken)) {
197197
yield return result;
198198
}
199199
await foreach (var result in _projectContentsConverter.GetAdditionalConversionResultsAsync(_additionalDocumentsToConvert, _cancellationToken)) {

CodeConverter/Common/SolutionConverter.cs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,17 +71,20 @@ private SolutionConverter(string solutionFilePath,
7171
public async IAsyncEnumerable<ConversionResult> ConvertAsync()
7272
{
7373
var projectsToUpdateReferencesOnly = _projectsToConvert.First().Solution.Projects.Except(_projectsToConvert);
74-
var solutionResult = string.IsNullOrWhiteSpace(_sourceSolutionContents) ? Enumerable.Empty<ConversionResult>() : ConvertSolutionFile().Yield();
75-
var convertedProjects = await ConvertProjectsAsync();
76-
var projectsAndSolutionResults = UpdateProjectReferences(projectsToUpdateReferencesOnly).Concat(solutionResult).ToAsyncEnumerable();
77-
await foreach (var p in convertedProjects.Concat(projectsAndSolutionResults)) {
78-
yield return p;
74+
75+
foreach (var project in _projectsToConvert) {
76+
await foreach (var result in ConvertProjectAsync(project).WithCancellation(_cancellationToken)) {
77+
yield return result;
78+
}
7979
}
80-
}
8180

82-
private async Task<IAsyncEnumerable<ConversionResult>> ConvertProjectsAsync()
83-
{
84-
return _projectsToConvert.ToAsyncEnumerable().SelectMany(ConvertProjectAsync);
81+
foreach (var result in UpdateProjectReferences(projectsToUpdateReferencesOnly)) {
82+
yield return result;
83+
}
84+
85+
if (!string.IsNullOrWhiteSpace(_sourceSolutionContents)) {
86+
yield return ConvertSolutionFile();
87+
}
8588
}
8689

8790
private IAsyncEnumerable<ConversionResult> ConvertProjectAsync(Project project)

Tests/Tests.csproj

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,13 @@
4141
<ItemGroup>
4242
<ProjectReference Include="..\CodeConv\CodeConv.csproj" />
4343
</ItemGroup>
44+
<!--
45+
The Vsix project is a net472 Windows-only project and only builds under an MSBuild that has
46+
the WindowsDesktop SDK available. We reference it so that VsixAssemblyCompatibilityTests can
47+
statically verify the Vsix output, but only in environments that can actually build it
48+
(i.e. Windows). Elsewhere the tests that depend on Vsix output will skip.
49+
-->
50+
<ItemGroup Condition="'$(OS)' == 'Windows_NT'">
51+
<ProjectReference Include="..\Vsix\Vsix.csproj" ReferenceOutputAssembly="false" SkipGetTargetFrameworkProperties="true" PrivateAssets="all" />
52+
</ItemGroup>
4453
</Project>

0 commit comments

Comments
 (0)