-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Description
Background and motivation
The .NET Runtime goes to great lengths to have binary compatibility between releases. That dramatically lowers the cost of version to version updates and provides a smooth update experience for customers. It also means that API decisions are forever, once shipped we are stuck with an API.
This is true even as the language and ecosystem evolves and better patterns emerge that we'd like to promote. For example consider the CallerArgumentExpression feature in C#. Given this feature it's possible to have a better default experience for Debug.Assert by defining the method as such:
public static class Debug
{
public static void Assert(bool condition, [CallerArgumentExpression("condition")] string message = null);
}This though is a bit of a non-starter. Due to the rules of C# this method will never be bound to because it will always prefer Debug.Assert(string). Changing the overload rules that make this determination is a bit of a non-starter because it would result in sweeping changes across the ecosystem. Where as the desired impact is much smaller: stop using Debug.Assert(string) for languages that support CallerArgumentExpression.
This desire to remove one method from source and replace with a highly compatible alternative has come up several times over the last few releases:
- Several interactions around
CallerArgumentExpressionAttribute - Desire for the CallerIdentityAttribute
- The C# 10 improved lambda overload resolution support revealed several regrettable overloads in ASP.NET that we wished could be replaced by more modern versions.
The proposal is to provide a mechanism by which APIs can be marked as "for binary compatibility only". The member will not be considered for source compilation purposes by languages.
This can be done by extending our existing ObsoleteAttribute to have a new property: BinaryCompatOnly.
Note that this is substantially different than ObsoleteAttribute.IsError. That in no way changes what members a language will bind to. Instead it is only considered after a language has gone through member look up, overload resolution, etc ... At the moment the final decision on a member is made only then does the language look at ObsoleteAttribute and determine a diagnostic should be emitted.
This proposal removes the API at a much lower level. It asks languages to not even consider the member during compilation. For all intents and purposes the member should not be imported from metadata except for what is necessary to ensure a type definition is sound (for example that it implements all interface members).
API Proposal
public sealed class ObsoleteAttribute : Attribute
{
public ObsoleteAttribute(string? message, bool error, bool binaryCompatOnly)
{
Message = message;
IsError = error;
BinaryCompatOnly = binaryCompatOnly;
}
public bool BinaryCompatOnly { get; set; }
}API Usage
public static class Debug
{
[Obsolete(BinaryCompatOnly = true)]
public static void Assert(bool condition);
public static void Assert(bool condition, [CallerArgumentExpression("condition")] string message = null);
}Alternative Designs
Leverage reference assemblies
An alternative design is to change the reference assembly generation tool such that these APIs are just not included. That would have roughly the same impact as this change: on upgrade only the new APIs would be available for compilation hence they would win out.
The downside of this approach is that it's going to be less friendly to languages other than C#, F# and VB. As detailed in CallerIdentityAttribute proposal it's likely that the alternative source methods provided are going to rely on new features. Those features are less likely to be present in other languages and hence is more likely to result in compilation errors on upgrade.
This is not the case with the proposal to add ObsoleteAttribute.BinaryCompateOnly. Languages have to make a change to recognize that attribute. Languages can then weigh the cost of supporting the features most relied on by BinaryCompateOnly and take them as a single feature in a release.
Alternate attribute
It's reasonable to see this as expanding ObsoleteAttribute beyond it's intended scope and instead a new attribute should be considered like BinaryCompateOnlyAttribute
Risks
Method Group Support
The motivating cases around this tend to be about overload resolution. Essentially an existing API is always preferred due to C#, VB, F# overload resolution rules and that is preventing us from adding a new, better API. The [Obsolete(BinaryCompatOnly = true)] in combination with a new overload, which is largely source compatible, will minimize upgrade experience pain to a large degree for method invocations.
// Compiles today, compiles tomorrow just to a diff overload
Debug.Assert(SomeCondition);This will not fix method group conversions though as they are focused on the signature of the method
// Compiles today, fails tomorrow
// Note: presence of [Conditional] makes this fail today but the general method group problem exists
Action<bool> action = Debug.Assert;This is likely a minority case compared to method invocation though and may be an acceptable trade off for forward progress in the platform.