Skip to content

Commit 4facf8a

Browse files
authored
Generate AsyncEnumerableDeprecated facade in runtime assembly (#2293)
1 parent 4c0104d commit 4facf8a

File tree

8 files changed

+514
-18
lines changed

8 files changed

+514
-18
lines changed

Ix.NET/Documentation/adr/0002-System-Linq-Async-In-Net10.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,23 @@ A further complication is that some methods in `System.Interactive.Async` clash
3636

3737
One more important point to consider is that although LINQ to `IAsyncEnumerable<T>` _mostly_ consists of extension methods, there are a few static methods. (E.g., `AsyncEnumerable.Range`, which the .NET library implements, and `AsyncEnumerable.Create`, which it does not.) With extension methods, the compiler does not have a problem with multiple identically-named types in different assemblies all defining extension methods as long as the individual methods do not conflict. However, non-extension methods are a problem. If `System.Linq.Async` were to continue to define a public `AsyncEnumerable` type, then calls to `AsyncEnumerable.Range` would fail to compile: even though there would only be a single `Range` method (supplied by the new `System.Linq.AsyncEnumerable`) this would fail to compile because `AsyncEnumerable` itself is an ambiguous class name. So it will be necessary for the public API of `System.Linq.Async` v7 not to define an `AsyncEnumerable` type. This places some limits on how far we can go with source-level compatibility. (Binary compatibility is not a problem because the runtime assemblies can continue to define this type.)
3838

39+
Since that constraint requires us to define a new type to hold all the obsolete extension methods (which we'll be calling `AsyncEnumerableDeprecated`) this creates a new problem: the runtime API needs to continue to provide all these methods as members of `AsyncEnumerable` (to provide binary compatibility) but any code newly compiled against Ix.NET v7 that is continuing to use these deprecated method (and which is therefore tolerating or suppressing the deprecation warning) will now end up building code that expects these methods to be in the `AsyncEnumerableDeprecated` class. We therefore need to provide all these methods in the runtime assemblies _twice_: once for binary compatibility as members of `AsyncEnumerable` (a class we completely hide at build time) and again as members of `AsyncEnumerableDeprecated` (the class we add to provide source-level backwards compatibility for code using the deprecated methods, in a way that doesn't cause ambiguous type name errors).
40+
3941

4042
## Decision
4143

4244
The next Ix.NET release will:
4345

4446
1. add a reference to `System.Linq.AsyncEnumerable` and `System.Interactive.Async` in `System.Linq.Async`
4547
2. remove from `System.Linq.Async`'s and `System.Interactive.Async`'s publicly visible API (ref assemblies) all `IAsyncEnumerable<T>` extension methods for which direct replacements exist (adding `MinByWithTiesAsync` and `MaxByWithTiesAsync` for the case where the new .NET runtime library methods actually have slightly different functionality)
46-
4. Rename `AsyncEnumerable` to `AsyncEnumerableDeprecated` in the public API (reference assemblies; the old name will be retained in runtime assemblies for binary compatibility) to avoid errors arising from there being two definitions of `AsyncEnumerable` in the same namespace
47-
5. add [Obsolete] attribute for members of `AsyncEnumerableDeprecated` for which `System.Linq.AsyncEnumerable` offers replacements that require code changes to use (e.g., `WhereAwait`, which is replaced by an overload of `Where`)
48-
6. the `AsyncEnumerable.ToEnumerable` method that was a bad idea and that should probably have never existed has been marked as `Obsolete` and will not be replaced; note that although `ToObservable` has issues that meant the .NET team decided not to replicate it, the main issue is that it embeds opinions, and not that there's anything fundamentally broken about it, so we do not include `ToObservable` in this category
49-
7. remaining methods of `AsyncEnumerable` (where `System.Linq.AsyncEnumerable` offers no equivalent) are removed from the publicly visible API of `System.Linq.Async`, with identical replacements being defined by `AsyncEnumerableEx` in `System.Interactive.Async`
50-
8. mark `IAsyncGrouping` as obsolete
51-
9. mark the public `IAsyncIListProvider` as obsolete, and define a non-public version for continued internal use in `System.Interactive.Linq`
52-
10. continue to provide the full `System.Linq.Async` API in the `lib` assemblies to provide binary compatibility
48+
3. Rename `AsyncEnumerable` to `AsyncEnumerableDeprecated` in the public API (reference assemblies; the old name will be retained in runtime assemblies for binary compatibility) to avoid errors arising from there being two definitions of `AsyncEnumerable` in the same namespace
49+
4. add [Obsolete] attribute for members of `AsyncEnumerableDeprecated` for which `System.Linq.AsyncEnumerable` offers replacements that require code changes to use (e.g., `WhereAwait`, which is replaced by an overload of `Where`)
50+
5. the `AsyncEnumerable.ToEnumerable` method that was a bad idea and that should probably have never existed has been marked as `Obsolete` and will not be replaced; note that although `ToObservable` has issues that meant the .NET team decided not to replicate it, the main issue is that it embeds opinions, and not that there's anything fundamentally broken about it, so we do not include `ToObservable` in this category
51+
6. remaining methods of `AsyncEnumerable` (where `System.Linq.AsyncEnumerable` offers no equivalent) are removed from the publicly visible API of `System.Linq.Async`, with identical replacements being defined by `AsyncEnumerableEx` in `System.Interactive.Async`
52+
7. mark `IAsyncGrouping` as obsolete
53+
8. mark the public `IAsyncIListProvider` as obsolete, and define a non-public version for continued internal use in `System.Interactive.Linq`
54+
9. continue to provide the full `System.Linq.Async` API in the `lib` assemblies to provide binary compatibility
55+
10. in the runtime `System.Linq.Async` assembly provide a facade that duplicates the legacy `AsyncEnumerable` methods on an `AsyncEnumerableDeprecated` type so that code that builds against `System.Linq.Async` v7, and which chooses to continue to use methods marked as `[Obsolete]`, will find those methods at runtime
5356
11. mark the `System.Linq.Async` NuGet package as obsolete, and recommend the use of `System.Linq.AsyncEnumerable` and/or `System.Interactive.Async` instead
5457

5558
The main effect of this is that code that had been using the `System.Linq.Async` implementation of LINQ for `IAsyncEnumerable<T>` will, in most cases, now be using the .NET runtime library implementation if it is rebuilt against this new version of `System.Linq.Async`.

Ix.NET/Source/Playground/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// See the LICENSE file in the project root for more information.
44

55
#pragma warning disable IDE0051 // Remove unused private members - all used via reflection
6+
#pragma warning disable CS0618 // Type or member is obsolete - this has not been updated since the deprecation of System.Linq.Async due to .NET 10's System.Linq.AsyncEnumerable
67

78
using System;
89
using System.Collections.Generic;

Ix.NET/Source/System.Linq.Async.SourceGenerator/AsyncOverloadsGenerator.cs

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
using System.Collections.Generic;
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT License.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Collections.Generic;
26
using System.IO;
37
using System.Text;
48
using Microsoft.CodeAnalysis;
@@ -12,7 +16,7 @@ namespace System.Linq.Async.SourceGenerator
1216
[Generator]
1317
public sealed class AsyncOverloadsGenerator : ISourceGenerator
1418
{
15-
private const string AttributeSource =
19+
private const string GenerateAsyncOverloadAttributeSource =
1620
"using System;\n" +
1721
"using System.Diagnostics;\n" +
1822
"namespace System.Linq\n" +
@@ -21,25 +25,47 @@ public sealed class AsyncOverloadsGenerator : ISourceGenerator
2125
" [Conditional(\"COMPILE_TIME_ONLY\")]\n" +
2226
" internal sealed class GenerateAsyncOverloadAttribute : Attribute { }\n" +
2327
"}\n";
28+
private const string DuplicateAsyncEnumerableAsAsyncEnumerableDeprecatedAttributeSource =
29+
"using System;\n" +
30+
"using System.Diagnostics;\n" +
31+
"namespace System.Linq\n" +
32+
"{\n" +
33+
" [AttributeUsage(AttributeTargets.Assembly)]\n" +
34+
" [Conditional(\"COMPILE_TIME_ONLY\")]\n" +
35+
" internal sealed class DuplicateAsyncEnumerableAsAsyncEnumerableDeprecatedAttribute : Attribute { }\n" +
36+
"}\n";
2437

2538
public void Initialize(GeneratorInitializationContext context)
2639
{
2740
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
28-
context.RegisterForPostInitialization(c => c.AddSource("GenerateAsyncOverloadAttribute", AttributeSource));
41+
context.RegisterForPostInitialization(c =>
42+
{
43+
c.AddSource("GenerateAsyncOverloadAttribute", GenerateAsyncOverloadAttributeSource);
44+
c.AddSource("DuplicateAsyncEnumerableAsAsyncEnumerableDeprecatedAttribute", DuplicateAsyncEnumerableAsAsyncEnumerableDeprecatedAttributeSource);
45+
});
2946
}
3047

3148
public void Execute(GeneratorExecutionContext context)
3249
{
3350
if (context.SyntaxReceiver is not SyntaxReceiver syntaxReceiver) return;
3451

3552
var options = GetGenerationOptions(context);
36-
var attributeSymbol = GetAsyncOverloadAttributeSymbol(context);
53+
var asyncOverloadAttributeSymbol = GetAsyncOverloadAttributeSymbol(context);
3754
var methodsBySyntaxTree = GetMethodsGroupedBySyntaxTree(context, syntaxReceiver);
3855

3956
foreach (var grouping in methodsBySyntaxTree.Where(g => g.Methods.Any()))
4057
context.AddSource(
4158
$"{Path.GetFileNameWithoutExtension(grouping.SyntaxTree.FilePath)}.AsyncOverloads",
42-
GenerateOverloads(grouping, options, context, attributeSymbol));
59+
GenerateOverloads(grouping, options, context, asyncOverloadAttributeSymbol));
60+
61+
var duplicateAsyncEnumerableAsAsyncEnumerableDeprecatedAttributeSymbol = GetDuplicateAsyncEnumerableAsAsyncEnumerableDeprecatedAttributeSymbol(context);
62+
var dupBuilder = new DeprecatedDuplicateBuilder(
63+
context,
64+
options,
65+
asyncOverloadAttributeSymbol,
66+
duplicateAsyncEnumerableAsAsyncEnumerableDeprecatedAttributeSymbol,
67+
syntaxReceiver);
68+
dupBuilder.BuildDuplicatesIfRequired();
4369
}
4470

4571
private static GenerationOptions GetGenerationOptions(GeneratorExecutionContext context)
@@ -95,7 +121,7 @@ private static string GenerateOverload(AsyncMethod method, GenerationOptions opt
95121
select a))))
96122
.Where(list => list.Attributes.Count > 0));
97123

98-
return MethodDeclaration(method.Syntax.ReturnType, GetMethodName(method.Symbol, options))
124+
return MethodDeclaration(method.Syntax.ReturnType, GetMethodNameForGeneratedAsyncMethod(method.Symbol, options))
99125
.WithAttributeLists(attributeListsWithGenerateAsyncOverloadRemoved)
100126
.WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword)))
101127
.WithTypeParameterList(method.Syntax.TypeParameterList)
@@ -117,8 +143,11 @@ private static string GenerateOverload(AsyncMethod method, GenerationOptions opt
117143
private static INamedTypeSymbol GetAsyncOverloadAttributeSymbol(GeneratorExecutionContext context)
118144
=> context.Compilation.GetTypeByMetadataName("System.Linq.GenerateAsyncOverloadAttribute") ?? throw new InvalidOperationException();
119145

146+
private static INamedTypeSymbol GetDuplicateAsyncEnumerableAsAsyncEnumerableDeprecatedAttributeSymbol(GeneratorExecutionContext context)
147+
=> context.Compilation.GetTypeByMetadataName("System.Linq.DuplicateAsyncEnumerableAsAsyncEnumerableDeprecatedAttribute") ?? throw new InvalidOperationException();
148+
120149
private static IEnumerable<AsyncMethodGrouping> GetMethodsGroupedBySyntaxTree(GeneratorExecutionContext context, SyntaxReceiver syntaxReceiver, INamedTypeSymbol attributeSymbol)
121-
=> from candidate in syntaxReceiver.Candidates
150+
=> from candidate in syntaxReceiver.CandidateMethods
122151
group candidate by candidate.SyntaxTree into grouping
123152
let model = context.Compilation.GetSemanticModel(grouping.Key)
124153
select new AsyncMethodGrouping(
@@ -128,7 +157,7 @@ from methodSyntax in grouping
128157
where methodSymbol.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass!, attributeSymbol))
129158
select new AsyncMethod(methodSymbol, methodSyntax));
130159

131-
private static string GetMethodName(IMethodSymbol methodSymbol, GenerationOptions options)
160+
internal static string GetMethodNameForGeneratedAsyncMethod(IMethodSymbol methodSymbol, GenerationOptions options)
132161
{
133162
var methodName = methodSymbol.Name.Replace("Core", "");
134163
return options.SupportFlatAsyncApi

0 commit comments

Comments
 (0)