Core .NET libraries breaking changes in .NET Core 1.0-3.0

The core .NET libraries provide the primitives and other general types used by .NET Core.

The following breaking changes are documented on this page:

Breaking change Version introduced
Passing GroupCollection to extension methods taking IEnumerable<T> requires disambiguation .NET Core 3.0
APIs that report version now report product and not file version .NET Core 3.0
Custom EncoderFallbackBuffer instances cannot fall back recursively .NET Core 3.0
Floating point formatting and parsing behavior changes .NET Core 3.0
Floating-point parsing operations no longer fail or throw an OverflowException .NET Core 3.0
InvalidAsynchronousStateException moved to another assembly .NET Core 3.0
Replacing ill-formed UTF-8 byte sequences follows Unicode guidelines .NET Core 3.0
TypeDescriptionProviderAttribute moved to another assembly .NET Core 3.0
ZipArchiveEntry no longer handles archives with inconsistent entry sizes .NET Core 3.0
FieldInfo.SetValue throws exception for static, init-only fields .NET Core 3.0
Path APIs don't throw an exception for invalid characters .NET Core 2.1
Private fields added to built-in struct types .NET Core 2.1
Change in default value of UseShellExecute .NET Core 2.1
OpenSSL versions on macOS .NET Core 2.1
UnauthorizedAccessException thrown by FileSystemInfo.Attributes .NET Core 1.0
Handling corrupted-process-state exceptions is not supported .NET Core 1.0
UriBuilder properties no longer prepend leading characters .NET Core 1.0
Process.StartInfo throws InvalidOperationException for processes you didn't start .NET Core 1.0

.NET Core 3.0

Passing GroupCollection to extension methods taking IEnumerable<T> requires disambiguation

When calling an extension method that takes an IEnumerable<T> on a GroupCollection, you must disambiguate the type using a cast.

Change description

Starting in .NET Core 3.0, System.Text.RegularExpressions.GroupCollection implements IEnumerable<KeyValuePair<String,Group>> in addition to the other types it implements, including IEnumerable<Group>. This results in ambiguity when calling an extension method that takes an IEnumerable<T>. If you call such an extension method on a GroupCollection instance, for example, Enumerable.Count, you'll see the following compiler error:

CS1061: 'GroupCollection' does not contain a definition for 'Count' and no accessible extension method 'Count' accepting a first argument of type 'GroupCollection' could be found (are you missing a using directive or an assembly reference?)

In previous versions of .NET, there was no ambiguity and no compiler error.

Version introduced

3.0

Reason for change

This was an unintentional breaking change. Because it has been like this for some time, we don't plan to revert it. In addition, such a change would itself be breaking.

For GroupCollection instances, disambiguate calls to extension methods that accept an IEnumerable<T> with a cast.

// Without a cast - causes CS1061.
match.Groups.Count(_ => true)

// With a disambiguating cast.
((IEnumerable<Group>)m.Groups).Count(_ => true);

Category

Core .NET libraries

Affected APIs

Any extension method that accepts an IEnumerable<T> is affected. For example:


APIs that report version now report product and not file version

Many of the APIs that return versions in .NET Core now return the product version rather than the file version.

Change description

In .NET Core 2.2 and previous versions, methods such as Environment.Version, RuntimeInformation.FrameworkDescription, and the file properties dialog for .NET Core assemblies reflect the file version. Starting with .NET Core 3.0, they reflect the product version.

The following figure illustrates the difference in version information for the System.Runtime.dll assembly for .NET Core 2.2 (on the left) and .NET Core 3.0 (on the right) as displayed by the Windows Explorer file properties dialog.

Difference in product version information

Version introduced

3.0

None. This change should make version detection intuitive rather than obtuse.

Category

Core .NET libraries

Affected APIs


Custom EncoderFallbackBuffer instances cannot fall back recursively

Custom EncoderFallbackBuffer instances cannot fall back recursively. The implementation of EncoderFallbackBuffer.GetNextChar() must result in a character sequence that is convertible to the destination encoding. Otherwise, an exception occurs.

Change description

During a character-to-byte transcoding operation, the runtime detects ill-formed or nonconvertible UTF-16 sequences and provides those characters to the EncoderFallbackBuffer.Fallback method. The Fallback method determines which characters should be substituted for the original nonconvertible data, and these characters are drained by calling EncoderFallbackBuffer.GetNextChar in a loop.

The runtime then attempts to transcode these substitution characters to the target encoding. If this operation succeeds, the runtime continues transcoding from where it left off in the original input string.

Previously, custom implementations of EncoderFallbackBuffer.GetNextChar() can return character sequences that are not convertible to the destination encoding. If the substituted characters cannot be transcoded to the target encoding, the runtime invokes the EncoderFallbackBuffer.Fallback method once again with the substitution characters, expecting the EncoderFallbackBuffer.GetNextChar() method to return a new substitution sequence. This process continues until the runtime eventually sees a well-formed, convertible substitution, or until a maximum recursion count is reached.

Starting with .NET Core 3.0, custom implementations of EncoderFallbackBuffer.GetNextChar() must return character sequences that are convertible to the destination encoding. If the substituted characters cannot be transcoded to the target encoding, an ArgumentException is thrown. The runtime will no longer make recursive calls into the EncoderFallbackBuffer instance.

This behavior only applies when all three of the following conditions are met:

  • The runtime detects an ill-formed UTF-16 sequence or a UTF-16 sequence that cannot be converted to the target encoding.
  • A custom EncoderFallback has been specified.
  • The custom EncoderFallback attempts to substitute a new ill-formed or nonconvertible UTF-16 sequence.

Version introduced

3.0

Most developers needn't take any action.

If an application uses a custom EncoderFallback and EncoderFallbackBuffer class, ensure the implementation of EncoderFallbackBuffer.Fallback populates the fallback buffer with well-formed UTF-16 data that is directly convertible to the target encoding when the Fallback method is first invoked by the runtime.

Category

Core .NET libraries

Affected APIs


Floating-point formatting and parsing behavior changed

Floating-point parsing and formatting behavior (by the Double and Single types) are now IEEE-compliant. This ensures that the behavior of floating-point types in .NET matches that of other IEEE-compliant languages. For example, double.Parse("SomeLiteral") should always match what C# produces for double x = SomeLiteral.

Change description

In .NET Core 2.2 and earlier versions, formatting with Double.ToString and Single.ToString, and parsing with Double.Parse, Double.TryParse, Single.Parse, and Single.TryParse are not IEEE-compliant. As a result, it's impossible to guarantee that a value will roundtrip with any supported standard or custom format string. For some inputs, the attempt to parse a formatted value can fail, and for others, the parsed value doesn't equal the original value.

Starting with .NET Core 3.0, floating-point parsing and formatting operations are IEEE 754-compliant.

The following table shows two code snippets and how the output changes between .NET Core 2.2 and .NET Core 3.1.

Code snippet Output on .NET Core 2.2 Output on .NET Core 3.1
Console.WriteLine((-0.0).ToString()); 0 -0
var value = -3.123456789123456789;
Console.WriteLine(value == double.Parse(value.ToString()));
False True

For more information, see the Floating-point parsing and formatting improvements in .NET Core 3.0 blog post.

Version introduced

3.0

The Potential impact to existing code section of the Floating-point parsing and formatting improvements in .NET Core 3.0 blog post suggests some changes you can make to your code if you want to maintain the previous behavior.

  • For some differences in formatting, you can get behavior equivalent to the previous behavior by specifying a different format string.
  • For differences in parsing, there's no mechanism to fall back to the previous behavior.

Category

Core .NET libraries

Affected APIs


Floating-point parsing operations no longer fail or throw an OverflowException

The floating-point parsing methods no longer throw an OverflowException or return false when they parse a string whose numeric value is outside the range of the Single or Double floating-point type.

Change description

In .NET Core 2.2 and earlier versions, the Double.Parse and Single.Parse methods throw an OverflowException for values that outside the range of their respective type. The Double.TryParse and Single.TryParse methods return false for the string representations of out-of-range numeric values.

Starting with .NET Core 3.0, the Double.Parse, Double.TryParse, Single.Parse, and Single.TryParse methods no longer fail when parsing out-of-range numeric strings. Instead, the Double parsing methods return Double.PositiveInfinity for values that exceed Double.MaxValue, and they return Double.NegativeInfinity for values that are less than Double.MinValue. Similarly, the Single parsing methods return Single.PositiveInfinity for values that exceed Single.MaxValue, and they return Single.NegativeInfinity for values that are less than Single.MinValue.

This change was made for improved IEEE 754:2008 compliance.

Version introduced

3.0

This change can affect your code in either of two ways:

  • Your code depends on the handler for the OverflowException to execute when an overflow occurs. In this case, you should remove the catch statement and place any necessary code in an If statement that tests whether Double.IsInfinity or Single.IsInfinity is true.

  • Your code assumes that floating-point values are not Infinity. In this case, you should add the necessary code to check for floating-point values of PositiveInfinity and NegativeInfinity.

Category

Core .NET libraries

Affected APIs


InvalidAsynchronousStateException moved to another assembly

The InvalidAsynchronousStateException class has been moved.

Change description

In .NET Core 2.2 and earlier versions, the InvalidAsynchronousStateException class is found in the System.ComponentModel.TypeConverter assembly.

Starting with .NET Core 3.0, it is found in the System.ComponentModel.Primitives assembly.

Version introduced

3.0

This change only affects applications that use reflection to load the InvalidAsynchronousStateException by calling a method such as Assembly.GetType or an overload of Activator.CreateInstance that assumes the type is in a particular assembly. If that is the case, update the assembly referenced in the method call to reflect the type's new assembly location.

Category

Core .NET libraries

Affected APIs

None.


Replacing ill-formed UTF-8 byte sequences follows Unicode guidelines

When the UTF8Encoding class encounters an ill-formed UTF-8 byte sequence during a byte-to-character transcoding operation, it replaces that sequence with a '�' (U+FFFD REPLACEMENT CHARACTER) character in the output string. .NET Core 3.0 differs from previous versions of .NET Core and the .NET Framework by following the Unicode best practice for performing this replacement during the transcoding operation.

This is part of a larger effort to improve UTF-8 handling throughout .NET, including by the new System.Text.Unicode.Utf8 and System.Text.Rune types. The UTF8Encoding type was given improved error handling mechanics so that it produces output consistent with the newly introduced types.

Change description

Starting with .NET Core 3.0, when transcoding bytes to characters, the UTF8Encoding class performs character substitution based on Unicode best practices. The substitution mechanism used is described by The Unicode Standard, Version 12.0, Sec. 3.9 (PDF) in the heading titled U+FFFD Substitution of Maximal Subparts.

This behavior only applies when the input byte sequence contains ill-formed UTF-8 data. Additionally, if the UTF8Encoding instance has been constructed with throwOnInvalidBytes: true, the UTF8Encoding instance will continue to throw on invalid input rather than perform U+FFFD replacement. For more information about the UTF8Encoding constructor, see UTF8Encoding(Boolean, Boolean).

The following table illustrates the impact of this change with an invalid 3-byte input:

Ill-formed 3-byte input Output before .NET Core 3.0 Output starting with .NET Core 3.0
[ ED A0 90 ] [ FFFD FFFD ] (2-character output) [ FFFD FFFD FFFD ] (3-character output)

The 3-char output is the preferred output, according to Table 3-9 of the previously linked Unicode Standard PDF.

Version introduced

3.0

No action is required on the part of the developer.

Category

Core .NET libraries

Affected APIs


TypeDescriptionProviderAttribute moved to another assembly

The TypeDescriptionProviderAttribute class has been moved.

Change description

In .NET Core 2.2 and earlier versions, The TypeDescriptionProviderAttribute class is found in the System.ComponentModel.TypeConverter assembly.

Starting with .NET Core 3.0, it is found in the System.ObjectModel assembly.

Version introduced

3.0

This change only affects applications that use reflection to load the TypeDescriptionProviderAttribute type by calling a method such as Assembly.GetType or an overload of Activator.CreateInstance that assumes the type is in a particular assembly. If that is the case, the assembly referenced in the method call should be updated to reflect the type's new assembly location.

Category

Windows Forms

Affected APIs

None.


ZipArchiveEntry no longer handles archives with inconsistent entry sizes

Zip archives list both compressed size and uncompressed size in the central directory and local header. The entry data itself also indicates its size. In .NET Core 2.2 and earlier versions, these values were never checked for consistency. Starting with .NET Core 3.0, they now are.

Change description

In .NET Core 2.2 and earlier versions, ZipArchiveEntry.Open() succeeds even if the local header disagrees with the central header of the zip file. Data is decompressed until the end of the compressed stream is reached, even if its length exceeds the uncompressed file size listed in the central directory/local header.

Starting with .NET Core 3.0, the ZipArchiveEntry.Open() method checks that local header and central header agree on compressed and uncompressed sizes of an entry. If they do not, the method throws an InvalidDataException if the archive's local header and/or data descriptor list sizes that disagree with the central directory of the zip file. When reading an entry, decompressed data is truncated to the uncompressed file size listed in the header.

This change was made to ensure that a ZipArchiveEntry correctly represents the size of its data and that only that amount of data is read.

Version introduced

3.0

Repackage any zip archive that exhibits these problems.

Category

Core .NET libraries

Affected APIs


FieldInfo.SetValue throws exception for static, init-only fields

Starting in .NET Core 3.0, an exception is thrown when you attempt to set a value on a static, InitOnly field by calling System.Reflection.FieldInfo.SetValue.

Change description

In .NET Framework and versions of .NET Core prior to 3.0, you could set the value of a static field that's constant after it is initialized (readonly in C#) by calling System.Reflection.FieldInfo.SetValue. However, setting such a field in this way resulted in unpredictable behavior based on the target framework and optimization settings.

In .NET Core 3.0 and later versions, when you call SetValue on a static, InitOnly field, a System.FieldAccessException exception is thrown.

Tip

An InitOnly field is one that can only be set at the time it's declared or in the constructor for the containing class. In other words, it's constant after it is initialized.

Version introduced

3.0

Initialize static, InitOnly fields in a static constructor. This applies to both dynamic and non-dynamic types.

Alternatively, you can remove the FieldAttributes.InitOnly attribute from the field, and then call FieldInfo.SetValue.

Category

Core .NET libraries

Affected APIs


.NET Core 2.1

Path APIs don't throw an exception for invalid characters

APIs that involve file paths no longer validate path characters or throw an ArgumentException if an invalid character is found.

Change description

In .NET Framework and .NET Core 1.0 - 2.0, the methods listed in the Affected APIs section throw an ArgumentException if the path argument contains an invalid path character. Starting in .NET Core 2.1, these methods no longer check for invalid path characters or throw an exception if an invalid character is found.

Reason for change

Aggressive validation of path characters blocks some cross-platform scenarios. This change was introduced so that .NET does not try to replicate or predict the outcome of operating system API calls. For more information, see the System.IO in .NET Core 2.1 sneak peek blog post.

Version introduced

.NET Core 2.1

If your code relied on these APIs to check for invalid characters, you can add a call to Path.GetInvalidPathChars.

Affected APIs

See also


Private fields added to built-in struct types

Private fields were added to certain struct types in reference assemblies. As a result, in C#, those struct types must always be instantiated by using the new operator or default literal.

Change description

In .NET Core 2.0 and previous versions, some provided struct types, for example, ConsoleKeyInfo, could be instantiated without using the new operator or default literal in C#. This was because the reference assemblies used by the C# compiler didn't contain the private fields for the structs. All private fields for .NET struct types are added to the reference assemblies starting in .NET Core 2.1.

For example, the following C# code compiles in .NET Core 2.0, but not in .NET Core 2.1:

ConsoleKeyInfo key;    // Struct type

if (key.ToString() == "y")
{
    Console.WriteLine("Yes!");
}

In .NET Core 2.1, the previous code results in the following compiler error: CS0165 - Use of unassigned local variable 'key'

Version introduced

2.1

Instantiate struct types by using the new operator or default literal.

For example:

ConsoleKeyInfo key = new ConsoleKeyInfo();    // Struct type.

if (key.ToString() == "y")
    Console.WriteLine("Yes!");
ConsoleKeyInfo key = default;    // Struct type.

if (key.ToString() == "y")
    Console.WriteLine("Yes!");

Category

Core .NET libraries

Affected APIs


Change in default value of UseShellExecute

ProcessStartInfo.UseShellExecute has a default value of false on .NET Core. On .NET Framework, its default value is true.

Change description

Process.Start lets you launch an application directly, for example, with code such as Process.Start("mspaint.exe") that launches Paint. It also lets you indirectly launch an associated application if ProcessStartInfo.UseShellExecute is set to true. On .NET Framework, the default value for ProcessStartInfo.UseShellExecute is true, meaning that code such as Process.Start("mytextfile.txt") would launch Notepad, if you've associated .txt files with that editor. To prevent indirectly launching an app on .NET Framework, you must explicitly set ProcessStartInfo.UseShellExecute to false. On .NET Core, the default value for ProcessStartInfo.UseShellExecute is false. This means that, by default, associated applications are not launched when you call Process.Start.

The following properties on System.Diagnostics.ProcessStartInfo are only functional when ProcessStartInfo.UseShellExecute is true:

This change was introduced in .NET Core for performance reasons. Typically, Process.Start is used to launch an application directly. Launching an app directly does not need to involve the Windows shell and incur the associated performance cost. To make this default case faster, .NET Core changes the default value of ProcessStartInfo.UseShellExecute to false. You can opt in to the slower path if you need it.

Version introduced

2.1

Note

In earlier versions of .NET Core, UseShellExecute was not implemented for Windows.

If your app relies on the old behavior, call Process.Start(ProcessStartInfo) with UseShellExecute set to true on the ProcessStartInfo object.

Category

Core .NET libraries

Affected APIs


OpenSSL versions on macOS

The .NET Core 3.0 and later runtimes on macOS now prefer OpenSSL 1.1.x versions to OpenSSL 1.0.x versions for the AesCcm, AesGcm, DSAOpenSsl, ECDiffieHellmanOpenSsl, ECDsaOpenSsl, RSAOpenSsl, and SafeEvpPKeyHandle types.

The .NET Core 2.1 runtime now supports OpenSSL 1.1.x versions, but still prefers OpenSSL 1.0.x versions.

Change description

Previously, the .NET Core runtime used OpenSSL 1.0.x versions on macOS for types that interact with OpenSSL. The most recent OpenSSL 1.0.x version, OpenSSL 1.0.2, is now out of support. To keep types that use OpenSSL on supported versions of OpenSSL, the .NET Core 3.0 and later runtimes now use newer versions of OpenSSL on macOS.

With this change, the behavior for the .NET Core runtimes on macOS is as follows:

  • The .NET Core 3.0 and later version runtimes use OpenSSL 1.1.x, if available, and fall back to OpenSSL 1.0.x only if there's no 1.1.x version available.

    For callers that use the OpenSSL interop types with custom P/Invokes, follow the guidance in the SafeEvpPKeyHandle.OpenSslVersion remarks. Your app may crash if you don't check the OpenSslVersion value.

  • The .NET Core 2.1 runtime uses OpenSSL 1.0.x, if available, and falls back to OpenSSL 1.1.x if there's no 1.0.x version available.

    The 2.1 runtime prefers the earlier version of OpenSSL because the SafeEvpPKeyHandle.OpenSslVersion property does not exist in .NET Core 2.1, so the OpenSSL version cannot be reliably determined at run time.

Version introduced

  • .NET Core 2.1.16
  • .NET Core 3.0.3
  • .NET Core 3.1.2

Category

Core .NET libraries

Affected APIs


.NET Core 1.0

UnauthorizedAccessException thrown by FileSystemInfo.Attributes

In .NET Core, an UnauthorizedAccessException is thrown when the caller attempts to set a file attribute value but doesn't have write permission.

Change description

In .NET Framework, an ArgumentException is thrown when the caller attempts to set a file attribute value in FileSystemInfo.Attributes but doesn't have write permission. In .NET Core, an UnauthorizedAccessException is thrown instead. (In .NET Core, an ArgumentException is still thrown if the caller attempts to set an invalid file attribute.)

Version introduced

1.0

Modify any catch statements to catch an UnauthorizedAccessException instead of, or in addition to, an ArgumentException, as necessary.

Category

Core .NET libraries

Affected APIs


Handling corrupted state exceptions is not supported

Handling corrupted-process-state exceptions in .NET Core is not supported.

Change description

Previously, corrupted-process-state exceptions could be caught and handled by managed code exception handlers, for example, by using a try-catch statement in C#.

Starting in .NET Core 1.0, corrupted-process-state exceptions cannot be handled by managed code. The common language runtime doesn't deliver corrupted-process-state exceptions to managed code.

Version introduced

1.0

Avoid the need to handle corrupted-process-state exceptions by addressing the situations that lead to them instead. If it's absolutely necessary to handle corrupted-process-state exceptions, write the exception handler in C or C++ code.

Category

Core .NET libraries

Affected APIs


UriBuilder properties no longer prepend leading characters

UriBuilder.Fragment no longer prepends a leading # character and UriBuilder.Query no longer prepends a leading ? character when one is already present.

Change description

In .NET Framework, the UriBuilder.Fragment and UriBuilder.Query properties always prepend a # or ? character, respectively, to the value being stored. This behavior can result in multiple # or ? characters in the stored value if the string already contains one of these leading characters. For example, the value of UriBuilder.Fragment might become ##main.

Starting in .NET Core 1.0, these properties no longer prepend the # or ? characters to the stored value if one is already present at the beginning of the string.

Version introduced

1.0

You no longer need to explicitly remove any of these leading characters when setting the property values. This is especially useful when you're appending values, because you no longer have to remove the leading # or ? each time you append.

For example, the following code snippet shows the behavior difference between .NET Framework and .NET Core.

var builder = new UriBuilder();
builder.Query = "one=1";
builder.Query += "&two=2";
builder.Query += "&three=3";
builder.Query += "&four=4";

Console.WriteLine(builder.Query);
  • In .NET Framework, the output is ????one=1&two=2&three=3&four=4.
  • In .NET Core, the output is ?one=1&two=2&three=3&four=4.

Category

Core .NET libraries

Affected APIs


Process.StartInfo throws InvalidOperationException for processes you didn't start

Reading the Process.StartInfo property for processes that your code didn't start throws an InvalidOperationException.

Change description

In .NET Framework, accessing the Process.StartInfo property for processes that your code didn't start returns a dummy ProcessStartInfo object. The dummy object contains default values for all of its properties except EnvironmentVariables.

Starting in .NET Core 1.0, if you read the Process.StartInfo property for a process that you didn't start (that is, by calling Process.Start), an InvalidOperationException is thrown.

Version introduced

1.0

Do not access the Process.StartInfo property for processes that your code didn't start. For example, don't read this property for processes returned by Process.GetProcesses.

Category

Core .NET libraries

Affected APIs