Skip to content

Commit 1005c28

Browse files
jaredparJared Parsonssharwellchsienkijjonescz
authored
Make OrderBy binary compat across TFMs (#68036)
* Make OrderBy binary compat across TFMs This puts `OrderBy` back into our Workspaces layer. It also addresses a number of other places where our `internal` API was not consistent with `public` API when polyfills were involved. Please consult the markdown doc for the rational here. That is going to be a repo artifact so lets focus the _why_ discussion there. Want it to be clear for future readers why this change is being made. * Apply suggestions from code review Co-authored-by: Sam Harwell <[email protected]> Co-authored-by: Chris Sienkiewicz <[email protected]> * pr feedback * Apply suggestions from code review Co-authored-by: Jan Jones <[email protected]> Co-authored-by: Fred Silberberg <[email protected]> * flip condition order --------- Co-authored-by: Jared Parsons <[email protected]> Co-authored-by: Sam Harwell <[email protected]> Co-authored-by: Chris Sienkiewicz <[email protected]> Co-authored-by: Jan Jones <[email protected]> Co-authored-by: Fred Silberberg <[email protected]>
1 parent 47840ae commit 1005c28

9 files changed

+146
-16
lines changed

docs/Target Framework Strategy.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
Target Framework Strategy
2+
===
3+
4+
# Layers
5+
The roslyn repository produces components for a number of different products that push varying ship and TFM constraints on us. A summary of some of our dependencies are :
6+
7+
- Build Tools: requires us to ship compilers on `net472`
8+
- .NET SDK: requires us to ship compilers on current servicing target framework (presently `net6.0`)
9+
- Source build: requires us to ship `$(NetCurrent)` and `$(NetPrevious)` in workspaces and below (presently `net8.0` and `net7.0` respectively)
10+
- Visual Studio: requires us to ship `net472` for base IDE components and `net6.0` for private runtime components.
11+
12+
It is not reasonable for us to take the union of all TFM and multi-target every single project to them. That would add several hundred compilations to any build operation which would in turn negatively impact our developer throughput. Instead we attempt to use the TFM where needed. That keeps our builds smaller but increases complexity a bit as we end up shipping a mix of TFM for binaries across our layers.
13+
14+
# Require consistent API across Target Frameworks
15+
It is important that our shipping APIs maintain consistent API surface area across target frameworks. That is true whether the API is `public` or `internal`.
16+
17+
The reason for `public` is standard design pattern. The reason for `internal` is a combination of the following problems:
18+
19+
- Our repository makes use of `InternalsVisibleTo` which allows other assemblies to directly reference signatures of `internal` members.
20+
- Our repository ships a mix of target frameworks. Typically workspaces and below will ship more recent TFMs than the layers above it. Compiler has to ship newer TFM for source build while IDE is constrained by Visual Studio's private runtime hence adopts newer TFM slower.
21+
- Our repository invests in polyfill APIs to make compiling against multiple TFMs in the same project a seamless experience.
22+
23+
Taken together though this means that our `internal` surface area in many cases is effectively `public` when it comes to binary compatibility. For example a consuming project can end up with `net7.0` binaries from workspaces layer and `net6.0` binaries from IDE layer. Because there is `InternalsVisibleTo` between these binaries the `internal` API surface area is effectively `public`. This requires us to have a consistent strategy for achieving binary compatibility across TFM combinations.
24+
25+
Consider a specific example of what goes wrong when our `internal` APIs are not consistent across TFM:
26+
27+
- Workspaces today targets `net6.0` and `net7.0` and it contains our `EnumerableExtensions.cs` which polyfills many extensions methods on `IEnumerable<T>`. In `net7.0` the `Order` extension method is not needed because it was put into the .NET core libraries.
28+
- Language Server Protocol targets `net6.0` and consumes the `Order` polyfill from Workspaces
29+
30+
Let's assume for a second that we `#if` the `Order` method such that it's not present in `net7.0`. Locally this all builds because we compile the `net6.0` versions against each other so they're consistent. However if an external project which targets `net7.0` and consumes both Workspaces and Language Server Protocol then it will be in a broken state. The Protocol binary is expecting Workspaces to contain a polyfill method for `Order` but it does not since it's at `net7.0` and it was `#if` out. As a result this will fail at runtime with missing method exceptions.
31+
32+
This problem primarily comes from our use of polyfill APIs. To avoid this we employ the following rule:
33+
34+
> When there is a `#if` directive that matches the regex `#if !?NET.*` that declares a non-private member, there must be an `#else` that defines an equivalent binary compatible symbol
35+
36+
This comes up in two forms:
37+
38+
## Pattern for types
39+
When creating a polyfill for a type use the `#if !NET...` to declare the type and in the `#else` use a `TypeForwardedTo` for the actual type.
40+
41+
Example:
42+
43+
```csharp
44+
#if NET6_0_OR_GREATER
45+
46+
using System.Runtime.CompilerServices;
47+
48+
#pragma warning disable RS0016 // Add public types and members to the declared API (this is a supporting forwarder for an internal polyfill API)
49+
[assembly: TypeForwardedTo(typeof(IsExternalInit))]
50+
#pragma warning restore RS0016 // Add public types and members to the declared API
51+
52+
#else
53+
54+
using System.ComponentModel;
55+
56+
namespace System.Runtime.CompilerServices
57+
{
58+
internal static class IsExternalInit
59+
{
60+
}
61+
}
62+
63+
#endif
64+
```
65+
66+
## Pattern for extension methods
67+
When creating a polyfill for an extension use the `#if NET...` to declare the extension method and the `#else` to declare the same method without `this`. That will put a method with the expected signature in the binary but avoids it appearing as an extension method within that target framework.
68+
69+
```csharp
70+
#if NET7_0_OR_GREATER
71+
public static IOrderedEnumerable<T> Order<T>(IEnumerable<T> source) where T : IComparable<T>
72+
#else
73+
public static IOrderedEnumerable<T> Order<T>(this IEnumerable<T> source) where T : IComparable<T>
74+
#endif
75+
```
76+
77+
78+
79+

src/Compilers/Core/Portable/InternalUtilities/CompilerFeatureRequiredAttribute.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@
55
// Copied from:
66
// https://github.com/dotnet/runtime/blob/fdd104ec5e1d0d2aa24a6723995a98d0124f724b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/CompilerFeatureRequiredAttribute.cs
77

8-
#if !NET7_0_OR_GREATER
8+
#if NET7_0_OR_GREATER
9+
10+
using System.Runtime.CompilerServices;
11+
12+
#pragma warning disable RS0016 // Add public types and members to the declared API (this is a supporting forwarder for an internal polyfill API)
13+
[assembly: TypeForwardedTo(typeof(CompilerFeatureRequiredAttribute))]
14+
#pragma warning restore RS0016 // Add public types and members to the declared API
15+
16+
#else
917

1018
namespace System.Runtime.CompilerServices
1119
{

src/Compilers/Core/Portable/InternalUtilities/EnumerableExtensions.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -513,12 +513,14 @@ public static IOrderedEnumerable<T> OrderByDescending<T>(this IEnumerable<T> sou
513513
return source.OrderByDescending(Comparer<T>.Create(compare));
514514
}
515515

516-
#if !NET7_0_OR_GREATER
516+
#if NET7_0_OR_GREATER
517+
public static IOrderedEnumerable<T> Order<T>(IEnumerable<T> source) where T : IComparable<T>
518+
#else
517519
public static IOrderedEnumerable<T> Order<T>(this IEnumerable<T> source) where T : IComparable<T>
520+
#endif
518521
{
519522
return source.OrderBy(Comparisons<T>.Comparer);
520523
}
521-
#endif
522524

523525
public static IOrderedEnumerable<T> ThenBy<T>(this IOrderedEnumerable<T> source, IComparer<T>? comparer)
524526
{

src/Compilers/Core/Portable/InternalUtilities/InterpolatedStringHandlerArgumentAttribute.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,15 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5-
#if !NET6_0_OR_GREATER
5+
#if NET6_0_OR_GREATER
6+
7+
using System.Runtime.CompilerServices;
8+
9+
#pragma warning disable RS0016 // Add public types and members to the declared API (this is a supporting forwarder for an internal polyfill API)
10+
[assembly: TypeForwardedTo(typeof(InterpolatedStringHandlerArgumentAttribute))]
11+
#pragma warning restore RS0016 // Add public types and members to the declared API
12+
13+
#else
614

715
namespace System.Runtime.CompilerServices
816
{

src/Compilers/Core/Portable/InternalUtilities/InterpolatedStringHandlerAttribute.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,15 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5-
#if !NET6_0_OR_GREATER
5+
#if NET6_0_OR_GREATER
6+
7+
using System.Runtime.CompilerServices;
8+
9+
#pragma warning disable RS0016 // Add public types and members to the declared API (this is a supporting forwarder for an internal polyfill API)
10+
[assembly: TypeForwardedTo(typeof(InterpolatedStringHandlerAttribute))]
11+
#pragma warning restore RS0016 // Add public types and members to the declared API
12+
13+
#else
614

715
namespace System.Runtime.CompilerServices
816
{

src/Compilers/Core/Portable/InternalUtilities/IsExternalInit.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@
55
// Copied from:
66
// https://github.com/dotnet/runtime/blob/218ef0f7776c2c20f6c594e3475b80f1fe2d7d08/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/IsExternalInit.cs
77

8-
#if !NET6_0_OR_GREATER
8+
#if NET6_0_OR_GREATER
9+
10+
using System.Runtime.CompilerServices;
11+
12+
#pragma warning disable RS0016 // Add public types and members to the declared API (this is a supporting forwarder for an internal polyfill API)
13+
[assembly: TypeForwardedTo(typeof(IsExternalInit))]
14+
#pragma warning restore RS0016 // Add public types and members to the declared API
15+
16+
#else
917

1018
using System.ComponentModel;
1119

@@ -21,12 +29,4 @@ internal static class IsExternalInit
2129
}
2230
}
2331

24-
#else
25-
26-
using System.Runtime.CompilerServices;
27-
28-
#pragma warning disable RS0016 // Add public types and members to the declared API (this is a supporting fowarder for an internal polyfill API)
29-
[assembly: TypeForwardedTo(typeof(IsExternalInit))]
30-
#pragma warning restore RS0016 // Add public types and members to the declared API
31-
3232
#endif

src/Compilers/Core/Portable/InternalUtilities/RequiredMemberAttribute.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@
55
// Copied from:
66
// https://github.com/dotnet/runtime/blob/fdd104ec5e1d0d2aa24a6723995a98d0124f724b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/RequiredMemberAttribute.cs
77

8-
#if !NET7_0_OR_GREATER
8+
#if NET7_0_OR_GREATER
9+
10+
using System.Runtime.CompilerServices;
11+
12+
#pragma warning disable RS0016 // Add public types and members to the declared API (this is a supporting forwarder for an internal polyfill API)
13+
[assembly: TypeForwardedTo(typeof(RequiredMemberAttribute))]
14+
#pragma warning restore RS0016 // Add public types and members to the declared API
15+
16+
#else
917

1018
namespace System.Runtime.CompilerServices
1119
{

src/Compilers/Core/Portable/InternalUtilities/SetsRequiredMembersAttribute.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,16 @@
55
// Copied from:
66
// https://github.com/dotnet/runtime/blob/fdd104ec5e1d0d2aa24a6723995a98d0124f724b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/SetsRequiredMembersAttribute.cs
77

8-
#if !NET7_0_OR_GREATER
8+
#if NET7_0_OR_GREATER
9+
10+
using System.Runtime.CompilerServices;
11+
using System.Diagnostics.CodeAnalysis;
12+
13+
#pragma warning disable RS0016 // Add public types and members to the declared API (this is a supporting forwarder for an internal polyfill API)
14+
[assembly: TypeForwardedTo(typeof(SetsRequiredMembersAttribute))]
15+
#pragma warning restore RS0016 // Add public types and members to the declared API
16+
17+
#else
918

1019
namespace System.Diagnostics.CodeAnalysis
1120
{

src/Compilers/Test/Core/ModuleInitializerAttribute.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,12 @@ public sealed class ModuleInitializerAttribute : Attribute
1212
}
1313
}
1414

15+
#else
16+
17+
using System.Runtime.CompilerServices;
18+
19+
#pragma warning disable RS0016 // Add public types and members to the declared API (this is a supporting forwarder for an internal polyfill API)
20+
[assembly: TypeForwardedTo(typeof(ModuleInitializerAttribute))]
21+
#pragma warning restore RS0016 // Add public types and members to the declared API
22+
1523
#endif

0 commit comments

Comments
 (0)