Skip to content

Commit 672f832

Browse files
authored
Handle clashes in System.Interactive.Async with System.Linq.Asyncenumerable (#2280)
1 parent 45ca03f commit 672f832

File tree

25 files changed

+1474
-114
lines changed

25 files changed

+1474
-114
lines changed

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,15 @@ There are also a couple of cases where functionality simply has not been reprodu
3232

3333
`System.Linq.Async` also defined some interfaces that are not replicated in `System.Linq.AsyncEnumerable`. `System.Linq.Async` defined `IAsyncGrouping` to act as the return type for `GroupBy`. `System.Linq.AsyncEnumerable` just uses `IAsyncEnumerable<IGrouping<TKey, TElement>>`, which is not quite the same: this enables asynchronous iteration of the sequence of groups, but each invidual group's contents are not asynchronously enumerable. `IAsyncGrouping` enabled asynchronous enumeration of both. In practice, `System.Linq.Async` did not exploit this: it fully enumerated the whole source list to split items into groups before returning the first group, so although it compelled you to enumerate at both levels (e.g., with nested `await foreach` loops), in reality only the outer level was asynchronous in practice. So this interface added complication without real benefits. There is also `IAsyncIListProvider<T>`, an interface that arguably should not have been public in the first place, serving only to enable some internal optimizations. (Apparently it was public in `System.Linq.Async` because it is also used in other parts of Ix.NET.)
3434

35+
A further complication is that some methods in `System.Interactive.Async` clash with methods in `System.Linq.AsyncEnumerable`. For example, `MaxByAsync` and `MinByAsync`. Originally `MinBy` and `MaxBy` were unique to Rx.NET and Ix.NET. But .NET 6.0 added operators with these names to LINQ to Objects. Confusingly, they were slightly different: the Rx.NET and Ix.NET versions recognize that there might not be a single minimum or maximum value, and thus provide a collection of all the entries that are at the maximum value, but the .NET runtime class library versions just pick one arbitrary winner. So at this point, `System.Interactive` renamed its versions to `MinByWithTies` and `MaxByWithTies`. Unfortunately that same change wasn't made in `System.Interactive.Async`, so we now have the same situation with `System.Linq.AsyncEnumerable`: the .NET runtime class libraries now define `MinByAsync` and `MaxByAsync` extension methods for `IAsyncEnumerable<T>`, and these take the same arguments as the ones in `System.Interactive.Async`, but have a different return type, and have different behaviour!
36+
37+
3538
## Decision
3639

37-
The next `System.Linq.Async` release will:
40+
The next Ix.NET release will:
3841

39-
1. add a reference to `System.Linq.AsyncEnumerable` and `System.Interactive.Async`
40-
2. remove from publicly visible API (ref assemblies) all `IAsyncEnumerable<T>` extension methods for which direct replacements exist
42+
1. add a reference to `System.Linq.AsyncEnumerable` and `System.Interactive.Async` in `System.Linq.Async`
43+
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)
4144
3. add [Obsolete] attribute for members of `AsyncEnumerable` for which `System.Linq.AsyncEnumerable` offers replacements that require code changes to use (e.g., `WhereAwait`, which is replaced by an overload of `Where`)
4245
4. `AsyncEnumerable` methods that are a bad idea and that should probably have never existing (the ones that do sync over async, e.g. `ToEnumerable`) are 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
4346
5. 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
@@ -60,6 +63,14 @@ In summary, each of the features previously provided by `System.Linq.Async` will
6063
* Method hidden in `ref` assembly, available in `System.Interactive.Async`
6164
* Method visible but marked as `Obsolete`, with new but slightly different equivalent available in `System.Linq.AsyncEnumerable`
6265

66+
### TFMs
67+
68+
We want to keep the TFMs for all Ix.NET packages exactly the same in this version, because the only reason for Ix.NET v7 to exist is to deal with the new existence of `System.Linq.AsyncEnumerable`.
69+
70+
There is one issue with this. If a project has a `net6.0` target and tries to use `System.Linq.AsyncEnumerable`, it produces a build warning, saying that it's not supported on that runtime. Although we don't like having this build warning, we are currently intending not to do anything about it, because we believe that messing with the TFMs is likely to have unintended consequences.
71+
72+
If it turns out that this does cause problems, we'll revisit this and do a new release.
73+
6374
## Consequences
6475

6576
Binary compatibility is maintained: any code that was built against `System.Linq.Async` v6 but which finds itself running against v7 at runtime should continue to work exactly as before.

Ix.NET/Source/ApiCompare/ApiCompare.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
55
<TargetFramework>net8.0</TargetFramework>
6+
<IsPackable>false</IsPackable>
67
</PropertyGroup>
78

89
<ItemGroup Condition="'$(TargetFramework)' != 'net10.0'">

Ix.NET/Source/AsyncQueryableGenerator.t4

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,10 @@ foreach (var m in asyncEnumerableType.GetMethods()
283283
cast = "(" + toQuoted(m.ReturnType, null, 0) + ")";
284284
}
285285
}
286+
else
287+
{
288+
continue; // this skips ToObservable, which we can't represent in a query
289+
}
286290
}
287291

288292
var n = 0;

Ix.NET/Source/Benchmarks.System.Interactive/Benchmarks.System.Interactive.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
55
<TargetFramework>net8.0</TargetFramework>
6+
<IsPackable>false</IsPackable>
67
<Optimize>true</Optimize>
78
<Configurations>Current Sources;Ix.net 3.1.1;Ix.net 3.2</Configurations>
89
</PropertyGroup>

Ix.NET/Source/FasterLinq/FasterLinq.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
55
<TargetFramework>net8.0</TargetFramework>
6+
<IsPackable>false</IsPackable>
67

78
<NoWarn>$(NoWarn);IDE0007;IDE0034;IDE0040;IDE0063;IDE0090;IDE1006</NoWarn>
89
</PropertyGroup>

Ix.NET/Source/Ix.NET.sln

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3-
# Visual Studio Version 17
4-
VisualStudioVersion = 17.2.32131.331
3+
# Visual Studio Version 18
4+
VisualStudioVersion = 18.0.11205.157 d18.0
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{87534290-A7A6-47A4-9A3A-D0D21A9AD1D4}"
77
ProjectSection(SolutionItems) = preProject
@@ -11,6 +11,7 @@ EndProject
1111
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B733D97A-F1ED-4FC3-BF8E-9AC47A89DE96}"
1212
ProjectSection(SolutionItems) = preProject
1313
..\..\.editorconfig = ..\..\.editorconfig
14+
AsyncQueryableGenerator.t4 = AsyncQueryableGenerator.t4
1415
..\..\azure-pipelines.ix.yml = ..\..\azure-pipelines.ix.yml
1516
CodeCoverage.runsettings = CodeCoverage.runsettings
1617
Directory.build.props = Directory.build.props
@@ -76,6 +77,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Linq.Async.SourceGen
7677
EndProject
7778
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.System.Interactive.ApiApprovals", "Tests.System.Interactive.ApiApprovals\Tests.System.Interactive.ApiApprovals.csproj", "{CD146918-6465-4D5B-B6B7-3F9803095EBD}"
7879
EndProject
80+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Interactive.Async", "refs\System.Interactive.Async\System.Interactive.Async.csproj", "{74613134-0799-190D-7023-EC9A28B3A1F2}"
81+
EndProject
7982
Global
8083
GlobalSection(SolutionConfigurationPlatforms) = preSolution
8184
Debug|Any CPU = Debug|Any CPU
@@ -156,6 +159,10 @@ Global
156159
{CD146918-6465-4D5B-B6B7-3F9803095EBD}.Debug|Any CPU.Build.0 = Debug|Any CPU
157160
{CD146918-6465-4D5B-B6B7-3F9803095EBD}.Release|Any CPU.ActiveCfg = Release|Any CPU
158161
{CD146918-6465-4D5B-B6B7-3F9803095EBD}.Release|Any CPU.Build.0 = Release|Any CPU
162+
{74613134-0799-190D-7023-EC9A28B3A1F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
163+
{74613134-0799-190D-7023-EC9A28B3A1F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
164+
{74613134-0799-190D-7023-EC9A28B3A1F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
165+
{74613134-0799-190D-7023-EC9A28B3A1F2}.Release|Any CPU.Build.0 = Release|Any CPU
159166
EndGlobalSection
160167
GlobalSection(SolutionProperties) = preSolution
161168
HideSolutionNode = FALSE
@@ -180,6 +187,7 @@ Global
180187
{1754B36C-D0DB-4E5D-8C30-1F116046DC0F} = {A3D72E6E-4ADA-42E0-8B2A-055B1F244281}
181188
{5C26D649-5ED4-49EE-AFBD-8FA8F12C4AE4} = {80EFE3A1-1414-42EA-949B-1B5370A1B2EA}
182189
{CD146918-6465-4D5B-B6B7-3F9803095EBD} = {87534290-A7A6-47A4-9A3A-D0D21A9AD1D4}
190+
{74613134-0799-190D-7023-EC9A28B3A1F2} = {A3D72E6E-4ADA-42E0-8B2A-055B1F244281}
183191
EndGlobalSection
184192
GlobalSection(ExtensibilityGlobals) = postSolution
185193
SolutionGuid = {AF70B0C6-C9D9-43B1-9BE4-08720EC1B7B7}

Ix.NET/Source/Playground/Playground.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
55
<TargetFramework>net8.0</TargetFramework>
6+
<IsPackable>false</IsPackable>
67
</PropertyGroup>
78

89
<ItemGroup>

0 commit comments

Comments
 (0)