diff --git a/accepted/2020/platform-exclusion/platform-exclusion.md b/accepted/2020/platform-exclusion/platform-exclusion.md new file mode 100644 index 000000000..377992a67 --- /dev/null +++ b/accepted/2020/platform-exclusion/platform-exclusion.md @@ -0,0 +1,372 @@ +# Annotating APIs as unsupported on specific platforms + +**PM** [Immo Landwerth](https://github.com/terrajobst) + +For .NET 5, we're building a feature which [annotates platform-specific +APIs][platform-checks]. The goal is to provide warnings when developers +accidentally call platform-specific APIs, thus limiting their ability to run on +multiple platforms, or if the code might run on earlier platform that doesn't +support the called API yet. + +This feature made the following assumption: + +* An unmarked API is considered to work on all OS platforms. +* An API marked with `[MinimumOSPlatform("...")]` is considered only portable to + the specified OS platforms (please note that the attribute can be applied + multiple times). + +This approach works well for cases where the API is really calling an +OS-specific API. Good examples include the Windows Registry or Apple-specific +graphic APIs. This is very different from browser support for specific HTML and +CSS features: those generally aren't browser specific, which is why browser +checks never worked well for the web. But for OS specific APIs, asking whether +you're actually running on that OS is very workable. The only fragile aspect +here is getting the version number right, but the analyzer enforces that. + +However, this approach doesn't work well for APIs that represent concepts that +aren't OS specific, such as threading or I/O. This is a problem for OS platforms +that are more constrained, such as Blazor WebAssembly, which runs in a browser +sandbox and thus can't freely create threads or open random files on disk. + +One could, in principle, apply the `[MinimumOSPlatform("...")]` attribute for +all other OS platforms that do support them, but this has several downsides: + +1. The so marked APIs are now considered platform-specific, which means that + just calling the API from portable code will now always be flagged by the + analyzer. +2. Every time .NET adds another operating system all call sites guarding calls + to these APIs now also have to check for this new OS. + +In this spec, we're proposing a mechanism by which we can mark APIs as +unsupported. + +## Scenarios and User Experience + +### Building a Blazor WebAssembly app + +Abelina is building a Blazor WebAssembly app. She's copy & pasting some code +from here ASP.NET Core web app that calls into an LDAP service using +`System.DirectoryServices.Protocols`. + +When doing so, she receives the following warning: + +> "LdapConnection" isn't supported for platform "browser" + +After doing some further reading Abelina decided to put the LDAP functionality +behind a web API that she calls from her Blazor WebAssembly app. + +### Building a class library + +Darrell works with Abelina and is tasked with moving some of the LDAP +functionality into a .NET Standard class library so that they can share the code +between their .NET Framework desktop app and their ASP.NET Core web API. + +When he compiles the class library he gets no warnings about unsupported Blazor +APIs. + +### Building a class library for use in Blazor WebAssembly + +Abelina and Darrel decide to centralize some of the functionality they use for +Razor views, specifically around URI and string processing. + +When consuming this library from Blazor WebAssembly they observe a +`PlatformNotSupported` exception. Upon further analysis, it seems they had a +dependency on `System.Drawing` that they weren't aware off. To avoid this, they +decide to enable the analyzer to check for APIs that are unsupported when +running inside a browser: + +```xml + + + +``` + +## Requirements + +### Goals + +* Be able to mark APIs that are unsupported on a particular OS +* Allows platforms to be considered supported by default with the ability to add + or remove platforms from the project file and SDK. +* By default, Blazor WebAssembly is only considered supported when building a + Blazor WebAssembly app. For regular class libraries (`netstandard` or + `net5.0`) the default assumes that it's not supported and thus won't generate + any warnings. However, the developer must still be able to include Blazor + WebAssembly manually. +* The annotations for unsupported APIs can express that an API was only + unsupported for a specific version. + +### Non-Goals + +* Allowing to express that APIs are unsupported in specific versions of the .NET + platform. + +## Design + +### Attribute + +The spec for [platform-checks] propose a set of attributes, specifically: + +* `MinimumOSPlatform` +* `ObsoletedInOSPlatform` +* `RemovedInOSPlatform` + +We suggest to rename `MinimumOSPlatform` to `SupportedOSPlatform` and +`RemovedInOSPlatform` to `UnsupportedOSPlatform`. + +```C# +namespace System.Runtime.Versioning +{ + [AttributeUsage(AttributeTargets.Assembly | + AttributeTargets.Class | + AttributeTargets.Constructor | + AttributeTargets.Enum | + AttributeTargets.Event | + AttributeTargets.Field | + AttributeTargets.Method | + AttributeTargets.Module | + AttributeTargets.Property | + AttributeTargets.Struct, + AllowMultiple = true, Inherited = false)] + public sealed class SupportedOSPlatformAttribute : OSPlatformAttribute + { + public SupportedOSPlatformAttribute(string platformName); + } + + [AttributeUsage(AttributeTargets.Assembly | + AttributeTargets.Class | + AttributeTargets.Constructor | + AttributeTargets.Enum | + AttributeTargets.Event | + AttributeTargets.Field | + AttributeTargets.Method | + AttributeTargets.Module | + AttributeTargets.Property | + AttributeTargets.Struct, + AllowMultiple = true, Inherited = false)] + public sealed class UnsupportedOSPlatformAttribute : OSPlatformAttribute + { + public UnsupportedOSPlatformAttribute(string platformName); + } +} +``` + +The semantics of these new attributes are as follows: + +* An API that doesn't have any of these attributes is considered supported by + all platforms. +* If either `[SupportedOSPlatform]` or `[UnsupportedOSPlatform]` attributes are + present, we group all attributes by OS platform identifier: + - **Allow list**. If the lowest version for each OS platform is a + `[SupportedOSPlatform]` attribute, the API is considered to *only* be + supported by the listed platforms and unsupported by all other platforms. + - **Deny list**. If the lowest version for each OS platform is a + `[UnsupportedOSPlatform]` attribute, then the API is considered to *only* + be unsupported by the listed platforms and supported by all other + platforms. + - **Inconsistent list**. If for some platforms the lowest version attribute + is `[SupportedOSPlatform]` while for others it is + `[UnsupportedOSPlatform]`, the analyzer will produce a warning on the API + definition because the API is attributed inconsistently. +* Both attributes can be instantiated without version numbers. This means the + version number is assumed to be `0.0`. This simplifies guard clauses, see + examples below for more details. +* `[ObsoletedInOSPlatform]` continuous to require a version number. +* `[ObsoletedInOSPlatform]` by itself doesn't imply support. However, it doesn't + make sense to apply `[ObsoletedInOSPlatform]` unless that platform is + supported. + +These semantics result in intuitive attribute applications with flexible rules. + +Let's consider the example where in .NET 5 we mark an API as being unsupported +in `Windows`. This will look as follows: + +```C# +[UnsupportedOSPlatform("windows")] +public void DoesNotWorkOnWindows(); +``` + +Now let's say that in .NET 6 we made the API work in Windows, but only on +Windows `10.0.1903`. We'd update the API as follows: + +```C# +[UnsupportedOSPlatform("windows")] +[SupportedOSPlatform("windows10.0.1903")] +public void DoesNotWorkOnWindows(); +``` + +The presence of `SupportedOSPlatform` still makes no claim for other platforms +because the lowest version for Windows says the API is unsupported for Windows. + +Similar to [platform-checks] this model still allows for OS vendors to obsolete +and remove APIs: + +```C# +[UnsupportedOSPlatform("windows")] +[SupportedOSPlatform("windows10.0.1903")] +[ObsoletedInOSPlatform("windows10.0.1909")] +[UnsupportedOSPlatform("windows10.0.2004")] +public void DoesNotWorkOnWindows(); +``` + +This describes an API that is supported by all platforms, except for Windows. +And on Windows, it's supported for Windows 10 1903 and 1909, but it was +obsoleted in 1909 and then became unsupported again in 2004. + +Platform-specific APIs like the iOS and Android bindings are still expressible +the same way: + +```C# +[SupportedOSPlatform("ios12.0")] +[ObsoletedInOSPlatform("ios13.0")] +[UnsupportedOSPlatform("ios14.0")] +[SupportedOSPlatform("ipados13.0")] +public void OnlyWorksOniOS(); +``` + +This API is considered to only work on iOS and iPadOS because the lowest version +starts with `[SupportedOSPlatform]`. + +### Analyzer behavior + +The existing analyzer that handles `[MinimumOSPlatform]` and +`[RemovedInOSPlatform]` will be modified to handle the new +`[SupportedOSPlatform]` and `[UnsupportedOSPlatform]` attributes instead, +following the semantics as outlined above. + +We'll change the analyzer to consider these two guard clauses as equivalent: + +```C# +if (RuntimeInformation.IsPlatform(OSPlatform.Windows)) +{ + WindowsSpecificApi(); +} + +if (RuntimeInformation.IsPlatformOrLater("windows0.0")) +{ + WindowsSpecificApi(); +} +``` + +The primary benefit for doing this is to support all the code that was written +since .NET Standard introduced the OS check APIs, which peopled used primarily +to guard Windows-specific APIs which are part of the otherwise OS-neutral +`netstandard` and `net5.0` TFMs. + +Let's look at a few examples of annotations what the corresponding guard clauses +are that would prevent the analyzer from flagging usage of the API. + +```C# +// The API is supported everywhere except when running in a browser +[UnsupportedOSPlatform("browser")] +public extern void Api(); + +public void Api_Usage() +{ + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Browser)) + { + Api(); + } +} +``` + +```C# +// On Windows, the API was unsupported until version 10.0.19041. +// The API is considered supported everywhere else without constraints. +[UnsupportedOSPlatform("windows")] +[SupportedOSPlatform("windows10.0.19041")] +public extern void Api(); + +public void Api_Usage() +{ + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || + RuntimeInformation.IsOSPlatformOrLater("windows10.0.19041")) + { + Api(); + } +} +``` + +```C# +// The API is only supported on Windows. It doesn't say which version, which +// means it effectively says since dawn of time. We'll use this to annotate +// Windows-specific APIs that will work on any version that can run .NET. +[SupportedOSPlatform("windows")] +public extern void Api(); + +public void Api_Usage() +{ + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Api(); + } +} +``` + +```C# +// The API is only supported on iOS. There, it started to be supported in +// version 12.0 and stopped being supported in version 14. +[SupportedOSPlatform("ios12.0")] +[UnsupportedOSPlatform("ios14.0")] +public extern void Api(); + +public void Api_Usage() +{ + if (RuntimeInformation.IsOSPlatformOrLater("ios12.0") && + !RuntimeInformation.IsOSPlatformOrLater("ios14.0")) + { + Api(); + } +} +``` + +### Build configuration for platforms + +In order to indicate which platforms the analyzer should warn about, we're +adding some metadata to MSBuild: + +```XML + + + + + + + +``` + +The targets of the Blazor WebAssembly SDK would initialize this as follows: + +```XML + + + + +``` + +These items needs to be passed to the invocation CSC as analyzer configuration. +AFAIK the only supported properties today, but we can have a target that +converts these items into a semicolon separated list of platforms. + +Using items instead of a property makes it easier for the developer to add or +remove from the set: + +When building a class library that is supposed to also work in the browser, a +developer can add the following to their project file: + +```XML + + + +``` + +This design also enables library developers to suppress warnings for platforms +that are normally supported by default: + +```XML + + + +``` + +[platform-checks]: ../platform-check/platform-checks.md