What's new in .NET 7 Preview 5 [WIP]
What's new in .NET 7 Preview 5
This issue is for teams to highlight work for the community that will release .NET 7 Preview 5
To add content, use a new conversation entry. The entry should include the team name and feature title as the first line as shown in the template below.
## Team Name: Feature title
[link to the tracking issue or epic item for the work]
Tell the story of the feature and anything the community should pay particular attention
to be successful using the feature.
Preview 1: https://github.com/dotnet/core/issues/7106 Preview 2: https://github.com/dotnet/core/issues/7107 Preview 3: https://github.com/dotnet/core/issues/7108 Preview 4: https://github.com/dotnet/core/issues/7378 Preview 5: https://github.com/dotnet/core/issues/7441 Preview 6: https://github.com/dotnet/core/issues/7454 Preview 7: https://github.com/dotnet/core/issues/7455
https://github.com/dotnet/runtime/pull/67917 by @steveharter
Update: see https://github.com/dotnet/core/issues/7441#issuecomment-1151296817 below
Observability
Expose performant ActivityEvent and ActivityLink tags enumerator methods https://github.com/dotnet/runtime/issues/68056
The exposed methods can be used in the perf critical scenarios to enumerate the Tags objects without any extra allocations and with fast items access.
namespace System.Diagnostics
{
public partial struct ActivityLink
{
public Activity.Enumerator<KeyValuePair<string, object?>> EnumerateTagObjects();
}
public partial struct ActivityEvent
{
public Activity.Enumerator<KeyValuePair<string, object?>> EnumerateTagObjects();
}
}
Sample Code
var tags = new List<KeyValuePair<string, object?>>()
{
new KeyValuePair<string, object?>("tag1", "value1"),
new KeyValuePair<string, object?>("tag2", "value2"),
};
ActivityLink link = new ActivityLink(default, new ActivityTagsCollection(tags));
foreach (ref readonly KeyValuePair<string, object?> tag in link.EnumerateTagObjects())
{
// Consume the link tags without any extra allocations or value copying.
}
ActivityEvent e = new ActivityEvent("SomeEvent", tags: new ActivityTagsCollection(tags));
foreach (ref readonly KeyValuePair<string, object?> tag in e.EnumerateTagObjects())
{
// Consume the event's tags without any extra allocations or value copying.
}
System.Text.Json
Polymorphism https://github.com/dotnet/runtime/issues/63747
System.Text.Json now supports serializing and deserializing polymorphic type hierarchies using attribute annotations:
[JsonDerivedType(typeof(Derived))]
public class Base
{
public int X { get; set; }
}
public class Derived : Base
{
public int Y { get; set; }
}
This configuration enables polymorphic serialization for Base, specifically when the runtime type is Derived:
Base value = new Derived();
JsonSerializer.Serialize<Base>(value); // { "X" : 0, "Y" : 0 }
Note that this does not enable polymorphic deserialization since the payload would be roundtripped as Base:
Base value = JsonSerializer.Deserialize<Base>(@"{ ""X"" : 0, ""Y"" : 0 }");
value is Derived; // false
Using Type Discriminators
To enable polymorphic deserialization, users need to specify a type discriminator for the derived class:
[JsonDerivedType(typeof(Base), typeDiscriminator: "base")]
[JsonDerivedType(typeof(Derived), typeDiscriminator: "derived")]
public class Base
{
public int X { get; set; }
}
public class Derived : Base
{
public int Y { get; set; }
}
Which will now emit JSON along with type discriminator metadata:
Base value = new Derived();
JsonSerializer.Serialize<Base>(value); // { "$type" : "derived", "X" : 0, "Y" : 0 }
which can be used to deserialize the value polymorphically:
Base value = JsonSerializer.Deserialize<Base>(@"{ ""$type"" : ""derived"", ""X"" : 0, ""Y"" : 0 }");
value is Derived; // true
Type discriminator identifiers can also be integers, so the following form is valid:
[JsonDerivedType(typeof(Derived1), 0)]
[JsonDerivedType(typeof(Derived2), 1)]
[JsonDerivedType(typeof(Derived3), 2)]
public class Base { }
JsonSerializer.Serialize<Base>(new Derived2()); // { "$type" : 1, ... }
Utf8JsonReader.CopyString https://github.com/dotnet/runtime/issues/54410
Until today, Utf8JsonReader.GetString() has been the only way users could consume decoded JSON strings. This will always allocate a new string, which might be unsuitable for certain performance-sensitive applications. The newly included CopyString methods allow copying the unescaped UTF-8 or UTF-16 strings to a buffer owned by the user:
int valueLength = reader.HasReadOnlySequence ? checked((int)ValueSequence.Length) : ValueSpan.Length;
char[] buffer = ArrayPool<char>.Shared.Rent(valueLength);
int charsRead = reader.CopyString(buffer);
ReadOnlySpan<char> source = buffer.Slice(0, charsRead);
ParseUnescapedString(source); // handle the unescaped JSON string
ArrayPool<char>.Shared.Return(buffer);
Or if handling UTF-8 is preferable:
ReadOnlySpan<byte> source = stackalloc byte[0];
if (!reader.HasReadOnlySequence && !reader.ValueIsEscaped)
{
source = reader.ValueSpan; // No need to copy to an intermediate buffer if value is span without escape sequences
}
else
{
int valueLength = reader.HasReadOnlySequence ? checked((int)ValueSequence.Length) : ValueSpan.Length;
Span<byte> buffer = valueLength <= 256 ? stackalloc byte[256] : new byte[valueLength];
int bytesRead = reader.CopyString(buffer);
source = buffer.Slice(0, bytesRead);
}
ParseUnescapedBytes(source);
Source Generation improvements
Added source generation support for IAsyncEnumerable<T>, JsonDocument and DateOnly/TimeOnly types.
System.IO.Stream
Add Stream ReadExactly and ReadAtLeast https://github.com/dotnet/runtime/issues/16598
One of the most common mistakes when using Stream.Read() is that the programmer doesn't realize that Read() may return less data than what is available in the Stream and less data than the buffer being passed in. And even for programmers who are aware of this, having to write the same loop every single time they want to read from a Stream is annoying.
To help this situation, we have added new methods to the base System.IO.Stream class:
namespace System.IO;
public partial class Stream
{
public void ReadExactly(Span<byte> buffer);
public void ReadExactly(byte[] buffer, int offset, int count);
public ValueTask ReadExactlyAsync(Memory<byte> buffer, CancellationToken cancellationToken = default);
public ValueTask ReadExactlyAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default);
public int ReadAtLeast(Span<byte> buffer, int minimumBytes, bool throwOnEndOfStream = true);
public ValueTask<int> ReadAtLeastAsync(Memory<byte> buffer, int minimumBytes, bool throwOnEndOfStream = true, CancellationToken cancellationToken = default);
}
The new ReadExactly methods are guaranteed to read exactly the number of bytes requested. If the Stream ends before the requested bytes have been read an EndOfStreamException is thrown.
Example
using FileStream f = File.Open("readme.md");
byte[] buffer = new byte[100];
f.ReadExactly(buffer); // guaranteed to read 100 bytes from the file
The new ReadAtLeast methods will read at least the number of bytes requested. It can read more if more data is readily available, up to the size of the buffer. If the Stream ends before the requested bytes have been read an EndOfStreamException is thrown. You can opt out of throwing an exception, however that means ReadAtLeast will read less than the requested bytes if the Stream ends.
Example
using FileStream f = File.Open("readme.md");
byte[] buffer = new byte[100];
int bytesRead = f.ReadAtLeast(buffer, 10);
// 10 <= bytesRead <= 100
New Roslyn analyzer and fixer for Regex source generator
https://github.com/dotnet/runtime/pull/69872/
In his blog post about Regular Expression performance, @stephentoub described the new source generator that will be shipping in .NET 7 to allow you to to statically generate regular expression logic at compile time. In order to take advantage of the source generator, you have to find places in your code where it could be used and make the necessary modification to the code in those places. This sounds like the perfect job for a Roslyn analyzer and fixer, and we've added this in Preview 5.
Analyzer
The new analyzer is inbox in .NET 7, and will search for uses of Regex that could be converted to use the Regex source generator instead. The analyzer will detect both, all uses of the Regex constructors, as well as all uses of the Regex static methods that qualify. In order for a use of Regex to be a candidate (and therefore, flagged by the analyzer) for conversion, the use must:
- Have all the parameters passed in to the constructor or the static method be constant at compile time. This is since the source generator's output depends on these parameters, so if their values are not constant at compile time, you can't use the source generator for that case.
- Target .NET 7. The analyzer ships inside the .NET 7 targeting pack, so only apps that target .NET 7 will be eligible for this analyzer.
- Use
LangVersionhigher to10. The regex source generator currently requiresLangVersionto be set topreviewin your project in order to be able to use the source generator, so the analyzer will only emit diagnostics if the project is using that version.
Here is an example of how the diagnostic is displayed in Visual Studio

Code Fixer
The code fixer is also shipping inbox in .NET 7, and will work for all diagnostics emitted by the analyzer. By clicking on "Show potential fixes" on the diagnostic logged by the analyzer, you will see an option called "Convert to 'RegexGenerator'". This is the code fixer and when you apply the changes, it will let you override the name to use for the generated method. It would also replace the invocation code that was causing the diagnostic with an invocation to the new source generated method.
Here is an example of how the code fixer gets applied in Visual Studio

We are hoping that this analyzer and fixer will help with the visibility of the source generator and will also make onboarding to it easier. Please try it out and let us know if you have any feedback.
Generic Math
In .NET 6 we previewed a feature known as "generic math": https://devblogs.microsoft.com/dotnet/preview-features-in-net-6-generic-math/. Since then, we have made continuous improvements to the implementation and responded to various feedback from the community in order to ensure that relevant scenarios are possible and the necessary APIs are available.
For more information on the changes and available APIs see https://devblogs.microsoft.com/dotnet/dotnet-7-generic-math/.
CodeGen
Community PRs
@singleaccretion made 23 PR contributions during Preview 5 with some highlighted items below:
- Improve the redundant branch optimization to handle more side effects PR#68447
- PUTARG_STK/x86: mark push [mem] candidates reg optional PR#68641
- Copy propagate on LCL_FLDs PR#68592
@Sandreenko completed allowing StoreLclVar src to be IND/FLD PR#59315. @hez2010 fixed CircleInConvex test in PR#68475.
See contributions from @anthonycanino, @aromaa and @ta264 in the below sections.
Arm64
- Arm64: Have CpBlkUnroll and InitBlkUnroll use SIMD registers uses SIMD registers for initialization of copy a block of memory less than 128 bytes. Some performance wins are shown here.

- PR#68363 consolidated 'msub' and 'madd' logic.
Loop Optimization
- PR#67930 Handle more scenarios for loop cloning now supports to operate on loops that go backwards or forwards with the increment of > 1. The performance wins can be seen here.

- PR#68588 Hoist the nullchecks for 'this' object moves the nullchecks on an object outside the loop. Performance wins can be seen here.

x86/x64 Optimizations
There were a number of optimizations targetted for x86/x64 platforms:
- PR#67182 Emit shlx, sarx, shrx on x64 optimized mov+shl, sar or shr to shlx, sarx or shrx on x64.
- PR#68091 enabled UMOD optimization for x64.
- @anthonycanino added X86Serialize hardware intrinsic in PR#68677.
- @aromaa optimizec bswap+mov to movbe in PR#66965.
- @ta264 fixed linux-x86 compilation for clr.alljits subset in PR#68046.
Modernize JIT
As more and more community contributors participate in the JIT code base, it is increasingly getting important to restructure our code and modernize it in order to allow our contributors to easily ramp up and rapidly develop code.
This release we did a lot of work on the internals of the JIT, cleaning up the JIT’s intermediate representation and removing limitations as a result of old design decisions. In many cases this work resulted in less memory usage and higher throughput of the JIT itself, while in other cases it resulted in better code quality. Some of these changes were:
The above allowed us to remove an old limitation in the JIT’s inliner when inlining functions with parameters of byte/sbyte/short/ushort type, resulting in better code quality:
- Allow the inliner to substitute for small arguments PR#69068
As .NET has become a more and more popular platform for writing high-performance code the coding style has changed. One area where the JIT was lacking was in its understanding of unsafe code involving reads and writes of structs and struct fields. @SingleAccretion contributed great changes in this area by switching the JIT’s internal model to a more general “physical” model. This paves the way for the JIT to better reason about unsafe code using features like struct reinterpretation.
Other minor cleanups were also made to simplify the JIT IR:
- Remove GTF_LATE_ARG PR#68617
- Substitute GT_RET_EXPR in inline candidate arguments PR#69117
- Remove stores as operands of calls in LIR PR#68460
General Optimizations
A few general JIT optimizations were done in below PRs:
I think we may also want to mention that crossgen2 now has been published with NativeAOT?
/cc: @agocke
An interesting question -- it does demonstrate that we're shipping programs using NativeAOT, but crossgen is also effectively private surface area, so I'm not sure if it would be helpful to most blog readers
System.Reflection performance improvements when invoking members
The overhead of using reflection to invoke a member (whether a method, constructor or a property getter\setter) has been substantially reduced when the invoke is done several times on the same member. Typical gains are 3-4x faster.
See https://github.com/dotnet/runtime/pull/67917 for more information.
Using the BenchmarkDotNet package:
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Reflection;
namespace ReflectionBenchmarks
{
internal class Program
{
static void Main(string[] args)
{
BenchmarkRunner.Run<InvokeTest>();
}
}
public class InvokeTest
{
private MethodInfo? _method;
private object[] _args = new object[1] { 42 };
[GlobalSetup]
public void Setup()
{
_method = typeof(InvokeTest).GetMethod(nameof(InvokeMe), BindingFlags.Public | BindingFlags.Static)!;
}
[Benchmark]
// This went from ~116ns to ~39ns or 3x (66%) faster.
public void InvokeSimpleMethod() => _method!.Invoke(obj: null, new object[] { 42 });
[Benchmark]
// This went from ~106ns to ~26ns or 4x (75%) faster.
public void InvokeSimpleMethodWithCachedArgs() => _method!.Invoke(obj: null, _args);
public static int InvokeMe(int i) => i;
}
}
Observability
Expose performant ActivityEvent and ActivityLink tags enumerator methods dotnet/runtime#68056
The exposed methods can be used in the perf critical scenarios to enumerate the Tags objects without any extra allocations and with fast items access.
namespace System.Diagnostics { public partial struct ActivityLink { public Activity.Enumerator<KeyValuePair<string, object?>> EnumerateTagObjects(); } public partial struct ActivityEvent { public Activity.Enumerator<KeyValuePair<string, object?>> EnumerateTagObjects(); } }
@tarekgh Can you please provide some sample code for the above?
@AngelosP I have updated my comment https://github.com/dotnet/core/issues/7441#issuecomment-1121772100 to include the code sample. Please let me know if you need anything else.
All submission and changes are in the latest draft, thank you all! <3
@AngelosP one minor erratum concerning the IAsyncEnumerable example:
// It now works and no longer throws NotSupportedException
JsonSerializer.Serialize(new MyPoco { Data = ... }, MyContext.MyPoco);
The above is not support and will always throw, since IAsyncEnumerable requires one of the async serialization methods. For the sake of correctness it should probably read:
// It now works and no longer throws NotSupportedException
await JsonSerializer.SerializeAsync(new MyPoco { Data = ... }, MyContext.MyPoco);
.NET 7 GA is available. Closing these pre-release issues.