Upraviť

Zdieľať cez


Change rules for compatibility

Throughout its history, .NET has attempted to maintain a high level of compatibility from version to version and across implementations of .NET. Although .NET 5 (and .NET Core) and later versions can be considered as a new technology compared to .NET Framework, two major factors limit the ability of this implementation of .NET to diverge from .NET Framework:

  • A large number of developers either originally developed or continue to develop .NET Framework applications. They expect consistent behavior across .NET implementations.
  • .NET Standard library projects allow developers to create libraries that target common APIs shared by .NET Framework and .NET 5 (and .NET Core) and later versions. Developers expect that a library used in a .NET application should behave identically to the same library used in a .NET Framework application.

Along with compatibility across .NET implementations, developers expect a high level of compatibility across versions of a given implementation of .NET. In particular, code written for an earlier version of .NET Core should run seamlessly on .NET 5 or a later version. In fact, many developers expect that the new APIs found in newly released versions of .NET should also be compatible with the pre-release versions in which those APIs were introduced.

This article outlines changes that affect compatibility and the way in which the .NET team evaluates each type of change. Understanding how the .NET team approaches possible breaking changes is particularly helpful for developers who open pull requests that modify the behavior of existing .NET APIs.

The following sections describe the categories of changes made to .NET APIs and their impact on application compatibility. Changes are either allowed (✔️), disallowed (❌), or require judgment and an evaluation of how predictable, obvious, and consistent the previous behavior was (❓).

Note

  • In addition to serving as a guide to how changes to .NET libraries are evaluated, library developers can also use these criteria to evaluate changes to their libraries that target multiple .NET implementations and versions.
  • For information about the compatibility categories, for example, forward and backwards compatibility, see How code changes can affect compatibility.

Modifications to the public contract

Changes in this category modify the public surface area of a type. Most of the changes in this category are disallowed since they violate backwards compatibility (the ability of an application that was developed with a previous version of an API to execute without recompilation on a later version).

Types

  • ✔️ ALLOWED: Removing an interface implementation from a type when the interface is already implemented by a base type

  • REQUIRES JUDGMENT: Adding a new interface implementation to a type

    This is an acceptable change because it does not adversely affect existing clients. Any changes to the type must work within the boundaries of acceptable changes defined here for the new implementation to remain acceptable. Extreme caution is necessary when adding interfaces that directly affect the ability of a designer or serializer to generate code or data that cannot be consumed down-level. An example is the ISerializable interface.

  • REQUIRES JUDGMENT: Introducing a new base class

    A type can be introduced into a hierarchy between two existing types if it doesn't introduce any new abstract members or change the semantics or behavior of existing types. For example, in .NET Framework 2.0, the DbConnection class became a new base class for SqlConnection, which had previously derived directly from Component.

  • ✔️ ALLOWED: Moving a type from one assembly to another

    The old assembly must be marked with the TypeForwardedToAttribute that points to the new assembly.

  • ✔️ ALLOWED: Changing a struct type to a readonly struct type

    Changing a readonly struct type to a struct type is not allowed.

  • ✔️ ALLOWED: Adding the sealed or abstract keyword to a type when there are no accessible (public or protected) constructors

  • ✔️ ALLOWED: Expanding the visibility of a type

  • DISALLOWED: Changing the namespace or name of a type

  • DISALLOWED: Renaming or removing a public type

    This breaks all code that uses the renamed or removed type.

    Note

    In rare cases, .NET may remove a public API. For more information, see API removal in .NET. For information about .NET's support policy, see .NET Support Policy.

  • DISALLOWED: Changing the underlying type of an enumeration

    This is a compile-time and behavioral breaking change as well as a binary breaking change that can make attribute arguments unparsable.

  • DISALLOWED: Sealing a type that was previously unsealed

  • DISALLOWED: Adding an interface to the set of base types of an interface

    If an interface implements an interface that it previously did not implement, all types that implemented the original version of the interface are broken.

  • REQUIRES JUDGMENT: Removing a class from the set of base classes or an interface from the set of implemented interfaces

    There is one exception to the rule for interface removal: you can add the implementation of an interface that derives from the removed interface. For example, you can remove IDisposable if the type or interface now implements IComponent, which implements IDisposable.

  • DISALLOWED: Changing a readonly struct type to a struct type

    The change of a struct type to a readonly struct type is allowed, however.

  • DISALLOWED: Changing a struct type to a ref struct type, and vice versa

  • DISALLOWED: Reducing the visibility of a type

    However, increasing the visibility of a type is allowed.

Members

  • ✔️ ALLOWED: Expanding the visibility of a member that is not virtual

  • ✔️ ALLOWED: Adding an abstract member to a public type that has no accessible (public or protected) constructors, or the type is sealed

    However, adding an abstract member to a type that has accessible (public or protected) constructors and is not sealed is not allowed.

  • ✔️ ALLOWED: Restricting the visibility of a protected member when the type has no accessible (public or protected) constructors, or the type is sealed

  • ✔️ ALLOWED: Moving a member into a class higher in the hierarchy than the type from which it was removed

  • ✔️ ALLOWED: Adding or removing an override

    Introducing an override might cause previous consumers to skip over the override when calling base.

  • ✔️ ALLOWED: Adding a constructor to a class, along with a parameterless constructor if the class previously had no constructors

    However, adding a constructor to a class that previously had no constructors without adding the parameterless constructor is not allowed.

  • ✔️ ALLOWED: Changing a member from abstract to virtual

  • ✔️ ALLOWED: Changing from a ref readonly to a ref return value (except for virtual methods or interfaces)

  • ✔️ ALLOWED: Removing readonly from a field, unless the static type of the field is a mutable value type

  • ✔️ ALLOWED: Calling a new event that wasn't previously defined

  • REQUIRES JUDGMENT: Adding a new instance field to a type

    This change impacts serialization.

  • DISALLOWED: Renaming or removing a public member or parameter

    This breaks all code that uses the renamed or removed member, or parameter.

    This includes removing or renaming a getter or setter from a property, as well as renaming or removing enumeration members.

  • REQUIRES JUDGMENT: Adding a member to an interface

    While it's a breaking change in the sense that it raises your minimum .NET version to .NET Core 3.0 (C# 8.0), which is when default interface members (DIMs) were introduced, adding a static, non-abstract, non-virtual member to an interface is allowed.

    If you provide an implementation, adding a new member to an existing interface won't necessarily result in compile failures in downstream assemblies. However, not all languages support DIMs. Also, in some scenarios, the runtime can't decide which default interface member to invoke. Beginning with C# 13, ref struct types can implement interfaces, but they can't be boxed or converted to an interface type. Therefore, a ref struct type must provide an explicit implementation for every instance interface member—it can't use the default implementation provided by the interface. Adding a default instance member to an interface that a ref struct implements requires the ref struct to add a corresponding implementation, which is a source breaking change. For these reasons, use judgment when adding a member to an existing interface.

    Note

    If your interface is implemented by ref struct types (possible in C# 13 and later), adding any default instance member to the interface is a source breaking change for those callers. The ref struct must provide an explicit implementation of the new member; it can't fall back to the default implementation.

  • DISALLOWED: Changing the value of a public constant or enumeration member

  • DISALLOWED: Changing the type of a property, field, parameter, or return value

  • DISALLOWED: Adding, removing, or changing the order of parameters

  • DISALLOWED: Adding or removing the in, out, or ref keyword from a parameter

  • ✔️ ALLOWED: Changing a ref parameter to ref readonly

    Changing a parameter from ref to ref readonly is source compatible for existing call sites that pass arguments with the ref modifier—those calls continue to compile without any change. Unlike changing ref to in, a ref readonly parameter doesn't silently allow callers to pass rvalues (non-variables); the compiler issues a warning if the argument isn't a variable. Existing ref call sites remain valid.

  • DISALLOWED: Changing an in parameter to ref readonly

    Call sites that pass in arguments without the in modifier (which the compiler allows for in parameters) will receive a warning when the parameter changes to ref readonly, because ref readonly requires the argument to be passed by reference. Callers that treat warnings as errors will experience a source breaking change.

  • DISALLOWED: Renaming a parameter (including changing its case)

    This is considered breaking for two reasons:

  • DISALLOWED: Changing from a ref return value to a ref readonly return value

  • ❌️ DISALLOWED: Changing from a ref readonly to a ref return value on a virtual method or interface

  • DISALLOWED: Adding or removing abstract from a member

  • DISALLOWED: Removing the virtual keyword from a member

  • DISALLOWED: Adding the virtual keyword to a member

    While this often is not a breaking change because the C# compiler tends to emit callvirt Intermediate Language (IL) instructions to call non-virtual methods (callvirt performs a null check, while a normal call doesn't), this behavior is not invariable for several reasons:

    • C# is not the only language that .NET targets.
    • The C# compiler increasingly tries to optimize callvirt to a normal call whenever the target method is non-virtual and is probably not null (such as a method accessed through the ?. null propagation operator).

    Making a method virtual means that the consumer code would often end up calling it non-virtually.

  • DISALLOWED: Making a virtual member abstract

    A virtual member provides a method implementation that can be overridden by a derived class. An abstract member provides no implementation and must be overridden.

  • DISALLOWED: Adding the sealed keyword to an interface member

    Adding sealed to a default interface member will make it non-virtual, preventing a derived type's implementation of that member from being called.

  • DISALLOWED: Adding an abstract member to a public type that has accessible (public or protected) constructors and that is not sealed

  • DISALLOWED: Adding or removing the static keyword from a member

  • DISALLOWED: Adding an overload that precludes an existing overload and defines a different behavior

    This breaks existing clients that were bound to the previous overload. For example, if a class has a single version of a method that accepts a UInt32, an existing consumer will successfully bind to that overload when passing a Int32 value. However, if you add an overload that accepts an Int32, when recompiling or using late-binding, the compiler now binds to the new overload. If different behavior results, this can be a breaking change.

  • REQUIRES JUDGMENT: Adding OverloadResolutionPriorityAttribute to an existing overload or changing its priority value

    The OverloadResolutionPriorityAttribute affects overload resolution at the source level: callers that recompile might resolve to a different overload than before. The intended use is to add the attribute to a new, better overload so the compiler prefers it over existing ones. Adding it to an existing overload or changing the priority value on an already-attributed overload can be a source breaking change because callers that recompile might change behavior.

  • ✔️ ALLOWED: Adding the allows ref struct anti-constraint to a generic type parameter

    Adding allows ref struct expands what types can be used as type arguments by permitting ref struct types. Existing callers using non-ref struct type arguments aren't affected. The generic method or type must follow ref safety rules for all instances of that type parameter.

  • DISALLOWED: Removing the allows ref struct anti-constraint from a generic type parameter

    Removing allows ref struct restricts which types callers can use as type arguments. Any caller that passes a ref struct as a type argument will no longer compile.

  • DISALLOWED: Adding a constructor to a class that previously had no constructor without adding the parameterless constructor

  • ❌️ DISALLOWED: Adding readonly to a field

  • DISALLOWED: Reducing the visibility of a member

    This includes reducing the visibility of a protected member when there are accessible (public or protected) constructors and the type is not sealed. If this is not the case, reducing the visibility of a protected member is allowed.

    Increasing the visibility of a member is allowed.

  • DISALLOWED: Changing the type of a member

    The return value of a method or the type of a property or field cannot be modified. For example, the signature of a method that returns an Object cannot be changed to return a String, or vice versa.

  • DISALLOWED: Adding an instance field to a struct that has no nonpublic fields

    If a struct has only public fields or has no fields at all, callers can declare locals of that struct type without calling the struct's constructor or first initializing the local to default(T), so long as all public fields are set on the struct before first use. Adding any new fields - public or nonpublic - to such a struct is a source breaking change for these callers, as the compiler will now require the additional fields to be initialized.

    Additionally, adding any new fields - public or nonpublic - to a struct with no fields or only public fields is a binary breaking change to callers that have applied [SkipLocalsInit] to their code. Since the compiler wasn't aware of these fields at compile time, it could emit IL that doesn't fully initialize the struct, leading to the struct being created from uninitialized stack data.

    If a struct has any nonpublic fields, the compiler already enforces initialization via the constructor or default(T), and adding new instance fields is not a breaking change.

  • DISALLOWED: Firing an existing event when it was never fired before

Behavioral changes

Assemblies

  • ✔️ ALLOWED: Making an assembly portable when the same platforms are still supported

  • DISALLOWED: Changing the name of an assembly

  • DISALLOWED: Changing the public key of an assembly

Properties, fields, parameters, and return values

  • ✔️ ALLOWED: Changing the value of a property, field, return value, or out parameter to a more derived type

    For example, a method that returns a type of Object can return a String instance. (However, the method signature cannot change.)

  • ✔️ ALLOWED: Increasing the range of accepted values for a property or parameter if the member is not virtual

    While the range of values that can be passed to the method or are returned by the member can expand, the parameter or member type cannot. For example, while the values passed to a method can expand from 0-124 to 0-255, the parameter type cannot change from Byte to Int32.

  • DISALLOWED: Increasing the range of accepted values for a property or parameter if the member is virtual

    This change breaks existing overridden members, which will not function correctly for the extended range of values.

  • DISALLOWED: Decreasing the range of accepted values for a property or parameter

  • DISALLOWED: Increasing the range of returned values for a property, field, return value, or out parameter

  • DISALLOWED: Changing the returned values for a property, field, method return value, or out parameter

  • DISALLOWED: Changing the default value of a property, field, or parameter

    Changing or removing a parameter default value is not a binary break. Removing a parameter default value is a source break, and changing a parameter default value could result in a behavioral break after recompilation.

    For this reason, removing parameter default values is acceptable in the specific case of "moving" those default values to a new method overload to eliminate ambiguity. For example, consider an existing method MyMethod(int a = 1). If you introduce an overload of MyMethod with two optional parameters a and b, you can preserve compatibility by moving the default value of a to the new overload. Now the two overloads are MyMethod(int a) and MyMethod(int a = 1, int b = 2). This pattern allows MyMethod() to compile.

  • DISALLOWED: Changing the precision of a numeric return value

  • REQUIRES JUDGMENT: A change in the parsing of input and throwing new exceptions (even if parsing behavior is not specified in the documentation

  • DISALLOWED: Adding or removing case types from a union declaration

    Adding or removing a case type from a union type is both a binary break and a source break.

    Pattern matching tests are no longer exhaustive after adding a case type. The compiler flags pattern matching expressions as non-exhaustive. At runtime, unexpected values cause runtime exceptions. Removing a case type removes the constructor declaration for that case type.

Exceptions

  • ✔️ ALLOWED: Throwing a more derived exception than an existing exception

    Because the new exception is a subclass of an existing exception, previous exception handling code continues to handle the exception. For example, in .NET Framework 4, culture creation and retrieval methods began to throw a CultureNotFoundException instead of an ArgumentException if the culture could not be found. Because CultureNotFoundException derives from ArgumentException, this is an acceptable change.

  • ✔️ ALLOWED: Throwing a more specific exception than NotSupportedException, NotImplementedException, NullReferenceException

  • ✔️ ALLOWED: Throwing an exception that is considered unrecoverable

    Unrecoverable exceptions should not be caught but instead should be handled by a high-level catch-all handler. Therefore, users are not expected to have code that catches these explicit exceptions. The unrecoverable exceptions are:

  • ✔️ ALLOWED: Throwing a new exception in a new code path

    The exception must apply only to a new code-path that's executed with new parameter values or state and that can't be executed by existing code that targets the previous version.

  • ✔️ ALLOWED: Removing an exception to enable more robust behavior or new scenarios

    For example, a Divide method that previously only handled positive values and threw an ArgumentOutOfRangeException otherwise can be changed to support both negative and positive values without throwing an exception.

  • ✔️ ALLOWED: Changing the text of an error message

    Developers should not rely on the text of error messages, which also change based on the user's culture.

  • DISALLOWED: Throwing an exception in any other case not listed above

  • DISALLOWED: Removing an exception in any other case not listed above

Attributes

  • ✔️ ALLOWED: Changing the value of an attribute that is not observable

  • DISALLOWED: Changing the value of an attribute that is observable

  • REQUIRES JUDGMENT: Removing an attribute

    In most cases, removing an attribute (such as NonSerializedAttribute) is a breaking change.

Platform support

  • ✔️ ALLOWED: Supporting an operation on a platform that was previously not supported

  • DISALLOWED: Not supporting or now requiring a specific service pack for an operation that was previously supported on a platform

Internal implementation changes

  • REQUIRES JUDGMENT: Changing the surface area of an internal type

    Such changes are generally allowed, although they break private reflection. In some cases, where popular third-party libraries or a large number of developers depend on the internal APIs, such changes may not be allowed.

  • REQUIRES JUDGMENT: Changing the internal implementation of a member

    These changes are generally allowed, although they break private reflection. In some cases, where customer code frequently depends on private reflection or where the change introduces unintended side effects, these changes may not be allowed.

  • ✔️ ALLOWED: Improving the performance of an operation

    The ability to modify the performance of an operation is essential, but such changes can break code that relies upon the current speed of an operation. This is particularly true of code that depends on the timing of asynchronous operations. The performance change should have no effect on other behavior of the API in question; otherwise, the change will be breaking.

  • ✔️ ALLOWED: Indirectly (and often adversely) changing the performance of an operation

    If the change in question is not categorized as breaking for some other reason, this is acceptable. Often, actions need to be taken that may include extra operations or that add new functionality. This will almost always affect performance but may be essential to make the API in question function as expected.

  • DISALLOWED: Changing a synchronous API to asynchronous (and vice versa)

Code changes

  • ✔️ ALLOWED: Adding params to a parameter

  • DISALLOWED: Changing a struct to a class and vice versa

  • DISALLOWED: Adding the checked statement to a code block

    This change might cause code that previously executed to throw an OverflowException and is unacceptable.

  • DISALLOWED: Removing params from a parameter

  • DISALLOWED: Changing the collection type of a params parameter

    Beginning with C# 13, params parameters support non-array collection types, including Span<T>, ReadOnlySpan<T>, struct or class types that implement IEnumerable<T> with an accessible parameterless constructor and an instance Add method, and specific interface types such as IList<T>. Changing the collection type of an existing params parameter (for example, from params T[] to params ReadOnlySpan<T>) changes the method's IL signature and is a binary breaking change. Callers compiled against the previous version must recompile.

  • ✔️ ALLOWED: Converting an extension method to the extension block member syntax

    Beginning with C# 14, you can declare extension members using extension blocks in addition to the older this-parameter syntax. Both forms generate identical IL, so callers can't distinguish between them. Converting existing extension methods to the new extension block syntax is binary and source compatible.

  • DISALLOWED: Changing the order in which events are fired

    Developers can reasonably expect events to fire in the same order, and developer code frequently depends on the order in which events are fired.

  • DISALLOWED: Removing the raising of an event on a given action

  • DISALLOWED: Changing the number of times given events are called

  • DISALLOWED: Adding the FlagsAttribute to an enumeration type

See also