Announcing .NET 7 Preview 5

Angelos Petropoulos

Today we released .NET 7 Preview 5. This preview of .NET 7 includes improvements to Generic Math which make the lives of API authors easier, a new Text Classification API for ML.NET that adds state-of-the-art deep learning techniques for natural language processing, various improvements to source code generators and a new Roslyn analyzer and fixer for RegexGenerator and multiple performance improvements in the areas of CodeGen, Observability, JSON serialization / deserialization and working with streams.

You can download .NET 7 Preview 5, for Windows, macOS, and Linux.

.NET 7 Preview 5 has been tested with Visual Studio 17.3 Preview 2. We recommend you use the preview channel builds if you want to try .NET 7 with Visual Studio family products. If you’re on macOS, we recommend using the latest Visual Studio 2022 for Mac preview. Now, let’s get into some of the latest updates in this release.

Observability

The goal of observability is to help you better understand the state of your application as scale and technical complexity increases.

#68056

The exposed methods can be used in performance critical scenarios to enumerate the Tag objects without any extra allocations and with fast items access.

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

#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

#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> (#59268), JsonDocument(#59954) and DateOnly/TimeOnly(#53539) types.

For example:

[JsonSerializable(typeof(typeof(MyPoco))]
public class MyContext : JsonSerializerContext {}

public class MyPoco
{
    // Use of IAsyncEnumerable that previously resulted 
    // in JsonSerializer.Serialize() throwing NotSupportedException 
    public IAsyncEnumerable<int> Data { get; set; } 
}

// It now works and no longer throws NotSupportedException
JsonSerializer.Serialize(new MyPoco { Data = ... }, MyContext.MyPoco); 

System.IO.Stream

ReadExactly and ReadAtLeast

#16598

One of the most common mistakes when using Stream.Read() is 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.

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 (in advanced cases when you want the benefits of ReadAtLest but you also want to handle the end-of-stream scenario yourself, you can opt out of throwing the exception).

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 RegexGenerator

#69872

In Regular Expression Improvements in .NET 7 Stephen Toub describes the new RegexGenerator source generator, which allows you to statically generate regular expressions at compile time resulting in better performance. To take advantage of this, first you have to find places in your code where it could be used and then make each code change. This sounds like the perfect job for a Roslyn analyzer and fixer, so we added one in Preview 5.

Analyzer

The new analyzer is included in .NET 7, and will search for uses of Regex that could be converted to use the RegexGenerator source generator instead. The analyzer will detect uses of the Regex constructors, as well as uses of the Regex static methods that meet the following criteria:

  • Parameters supplied have a known value at compile time. The source generator’s output depends on these values, so they must be known at compile time.
  • They are part of an app that targets .NET 7. The new analyzer ships inside the .NET 7 targeting pack and only apps targeting .NET 7 are eligible for this analyzer.
  • The LangVersion (learn more) is higher than 10. For the time being the regex source generator requires LangVersion to be set to preview.

Here is the new analyzer in action in Visual Studio:

Image of new analyzer in action in Visual Studio

Code fixer

The code fixer is also included in .NET 7 and it does two things. First, it suggests a RegexGenerator source generator method and gives you the option to override the default name. Then it replaces the original code with a call to the new method.

Here is the new code fixer in action in Visual Studio:

Image of new code fixer in action in Visual Studio

Generic Math

In .NET 6 we previewed a feature called Generic Math which allows .NET developers to take advantage of static APIs, including operators, from within generic code. This feature will directly benefit API authors who can simplify their codebase. Other developers will benefit indirectly as the APIs they consume will start supporting more types without the requirement for each and every numeric type to get explicit support.

In .NET 7 we have made improvements to the implementation and responded to feedback from the community. For more information on the changes and available APIs please see our Generic Math specific announcement.

System.Reflection performance improvements when invoking members

#67917

The overhead of using reflection to invoke a member (whether a method, constructor or a property gettersetter) has been substantially reduced when the invoke is done several times on the same member. Typical gains are 3-4x faster.

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;
    }
}

ML.NET Text Classification API

Text classification is the process of applying labels or categories to text.

Common use cases include:

  • Categorizing e-mail as spam or not spam
  • Analyzing sentiment as positive or negative from customer reviews
  • Applying labels to support tickets

Text classification is a subset of classification, so today you could solve text classification problems with the existing classification algorithms in ML.NET. However, those algorithms don’t address common challenges with text classification as well as modern deep learning techniques.

We are excited to introduce the ML.NET Text Classification API, an API that makes it easier for you to train custom text classification models and brings the latest state-of-the-art deep learning techniques for natural language processing to ML.NET.

For more details please see our ML.NET specific announcement

CodeGen

Many thanks to the community contributors.

@singleaccretion made 23 PR contributions during Preview 5 with the highlights being:

  • Improve the redundant branch optimization to handle more side effects #68447
  • PUTARG_STK/x86: mark push [mem] candidates reg optional #68641
  • Copy propagate on LCL_FLDs #68592

@Sandreenko completed allowing StoreLclVar src to be IND/FLD #59315. @hez2010 fixed CircleInConvex test in #68475.

More contributions from @anthonycanino, @aromaa and @ta264 are included in the sections to follow.

Arm64

#68363 consolidated ‘msub’ (multiplies two register values, subtracts the product from a third register value) and ‘madd’ (multiplies two register values, adds a third register value) logic.

Arm64: Have CpBlkUnroll and InitBlkUnroll use SIMD registers for initialization of copying a block of memory smaller than 128 bytes (see perf improvement details).

CpBlkUnroll and InitBlkUnroll perf improvements

Loop Optimization

#67930 Handle more scenarios for loop cloning now supports loops that go backwards or forwards with the increment of > 1 (see perf improvement details).

Loop Optimization perf improvements

#68588 Hoist the nullchecks for ‘this’ object moves the nullchecks on an object outside the loop (see perf improvement details).

nullchecks perf improvements

x86/x64 Optimizations

General Optimizations

  • PR#68105 enabled multiple nested “no GC” region requests.
  • PR#69034 removed “promoted parameter” tailcall limitation.

Modernize JIT

As the community has ramped up its contributions to the JIT code base, it has become important to restructure and modernize our codebase to enable our contributors to easily ramp up and rapidly develop code.

In Preview 5 we did a lot of work on the internals, cleaned up the JIT’s intermediate representation and removed limitations imposed by past 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. Here are some highlights:

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 #69068)

One area that needed improvement was better 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:

  • Physical value numbering #68712
  • Implement constant-folding for VNF_BitCast #68979

Other minor cleanups were also made to simplify the JIT IR:

  • Remove GTF_LATE_ARG #68617
  • Substitute GT_RET_EXPR in inline candidate arguments #69117
  • Remove stores as operands of calls in LIR #68460

Enable library trimming

As we’ve previously described, trimming lets the SDK remove unused code from your self-contained apps to make them smaller. However, trim warnings can indicate that an app is not compatible with trimming. To make apps compatible, all their references also have to be compatible.

For that, we need libraries to adopt trimming as well. In Preview 5, we’ve worked to make finding and fixing trim warnings in libraries even easier using Roslyn analyzers. To turn see trim warnings for your library, add <IsTrimmable>true</IsTrimmable> to your project file. After fixing the warnings, trimmed apps using your library will be smaller and compatible with trimming. See Prepare .NET libraries for trimming – .NET | Microsoft Docs for more info about library trimming.

Contributor spotlight: Steve Dunn

Steve documented the beginning of his contributions to .NET and we are grateful for all of them. We want to take the opportunity to specifically call out Steve’s work on Microsoft.Extensions.Configuration. He’s taken on high-impact issues that require significant technical knowledge and domain expertise and a good example of this is support immutable types with configuration binding.

Not only is Steve up to the technical challenge, but he is also understanding, patient and thoughtful. In his most recent PR Steve received 123 comments from six reviews, and involved 41 commits. Steve welcomed the feedback and took initiative to achieve a resolution that everybody was happy with. Steve is a perfect example of a high-quality community contributor.

steve-dunn

In Steve’s own words:

I currently write services in .NET for corporate clients. In the past I’ve published packaged products, including Arabian Themes, which was an Arabic conversion of “Windows 95 Plus!”

I started writing software in the mid 80’s, beginning with BBC Basic and then Commodore 64 games, including Better dead than alien, Call me psycho, Space relief, Galaxia 7, Thunder Hawk and Zone Z

I regularly write articles on https://dunnhq.com and I’m currently working on curing Primitive Obsession with Vogen.

Contributor spotlight: Anthony Canino

Anthony recently added X86Serialize hardware intrinsic based on an API proposal that exposes xarch serialize instruction referencing a couple of chapters of Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A and Intel Architecture Instruction Set Extensions and Future Features.

This allows a developer to issue X86Serialize.Serialize() before a conditional such as:

X86Serialize.Serialize();
if (someCondition)
{
  // ...
}

Thank you Anthony for your multiple contributions to .NET!

anthony-canino

In Anthony’s own words:

My name is Anthony Canino and I am a Software Engineer at Intel, based in Seattle Washington, with a passion for compiler development and language design. As a newly located PNW resident, I enjoy taking advantage of all the wonderful hiking and beautiful nature the area has to offer with my two Siberian huskies.

Our team at Intel believes in developer-experience first, and we support the .NET ecosystem and wish to continue to see the platform thrive and serve as the developer-focused centerpiece that it is today. Some of my first contributions to .NET were some peephole optimizations in the xarch code emitter that really helped me to understand some of the lower-level details of code generation. Specifically to .NET’s RyuJIT, I’ve found the quality of the code and the documentation significantly reduce the barrier of entry toward making contributions in a challenging space.

What I love about contributing to .NET is the dedication that Microsoft has put forth to enable open source developers to participate in the project with production-level resources — documentation and testing infrastructure and pipeline — and professional developer community support and planning. This kind of treatment really allows external contributors to feel part of the overall direction of the project and encourages further contributions to the ecosystem.

Targeting .NET 7

To target .NET 7, you need to use a .NET 7 Target Framework Moniker (TFM) in your project file. For example:

<TargetFramework>net7.0</TargetFramework>

The full set of .NET 7 TFMs, including operating-specific ones follows.

  • net7.0
  • net7.0-android
  • net7.0-ios
  • net7.0-maccatalyst
  • net7.0-macos
  • net7.0-tvos
  • net7.0-windows

We expect that upgrading from .NET 6 to .NET 7 should be straightforward. Please report any breaking changes that you discover in the process of testing existing apps with .NET 7.

Support

.NET 7 is a Short Term Support (STS) release, meaning it will receive free support and patches for 18 months from the release date. It’s important to note that the quality of all releases is the same. The only difference is the length of support. For more about .NET support policies, see the .NET and .NET Core official support policy.

We recently recently changed the “Current” name to “Short Term Support (STS)”. We’re in the process of rolling out that change.

Breaking changes

You can find the most recent list of breaking changes in .NET 7 by reading the Breaking changes in .NET 7 document. It lists breaking changes by area and release with links to detailed explanations.

To see what breaking changes are proposed but still under review, follow the Proposed .NET Breaking Changes GitHub issue.

Roadmaps

Releases of .NET include products, libraries, runtime, and tooling, and represent a collaboration across multiple teams inside and outside Microsoft. You can learn more about these areas by reading the product roadmaps:

Closing

We appreciate and thank you for your all your support and contributions to .NET. Please give .NET 7 Preview 5 a try and tell us what you think!

21 comments

Discussion is closed. Login to edit/delete existing comments.

  • Neil Mosafi 0

    Great summary Angelos and a nice surprise to see your name here!

    • Angelos PetropoulosMicrosoft employee 0

      Hey Neil, what an awesome surprise. It’s been so long 🙂 I still think (and talk about) our times together constantly – robotic arms and web services … web services everywhere!

      • Steve Dunn 0

        Thanks for the mention! Neil and I worked together as well, many moons back!

  • James Wil 0

    nice to see community contributors getting the spotlight!
    thanks for their contributions!

  • Mon Minh 0

    Why Vs 2022 preview 4 is missing MAUI template?

    • Ed Charbeneau 0

      Because you installed .NET 7 Preview. So much for side-by-side installation.

      • Mon Minh 0

        Thanks

  • JinShil 0

    Why is there no net7.0-linux TFM?

    • Alex Lambert 0

      Because it is not needed. There are no APIs in .NET that only work on linux. E.g. the net7.0-windows is only needed on windows if you need to access windows specific APIs, like WPF. So you can just use net7.0 on Linux

    • Hughes, Danial 0

      Good luck getting any kind of response with anything WPF related. Microsoft do not care about all of the existing LOB enterprise apps written in WPF, but they want everyone to embrace the ‘new’ stuff. The company with a 1.6TRILLION market cap won’t even staff the WPF team with a few devs to manage pull requests from the community. Pretty sad.

      • anonymous 0

        this comment has been deleted.

    • Gurpreet SinghMicrosoft employee 0

      We have read through all the comments in this post and acknowledge your feedback regarding the frustration in merging of PRs. Due to large number of integration tests which need to be run manually for each PR, you see a relatively long lead time for feedback. However, we are taking steps to address this and we are focused on modernizing our Test infrastructure to improve the turn-around times on PRs.

      We are grateful for all your contributions and look forward to that in the future as well. For more details, please refer:
      https://github.com/dotnet/wpf/discussions/6542#discussioncomment-2969422

  • Jiří Zídek 0

    Ad: Observability – I more and more miss some basic yet performant way to write logs to files (using ILogger) from Microsoft. Full blown logging frameworks are real overkill or bloatware.

    • Angelos PetropoulosMicrosoft employee 0

      What did you have in mind? I too sometimes don’t want to use a full blown logging framework and I turn to my personal logging library that has some handy overloads. Is there some specific functionality you’d have to build into your personal logging library that you’d rather get out of the box?

  • Dale Sinder 0

    Updated my WASM hosted app from preview 4 to 5. This occurs on startup:

    Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware: Error: An unhandled exception has occurred while executing the request.

    System.InvalidOperationException: No output has been recevied from the application.
    at Microsoft.AspNetCore.Builder.DebugProxyLauncher.LaunchAndGetUrl(IServiceProvider serviceProvider, String devToolsHost)
    at Microsoft.AspNetCore.Builder.WebAssemblyNetDebugProxyAppBuilderExtensions.<>c.<b__0_1>d.MoveNext()
    — End of stack trace from previous location —
    at Microsoft.AspNetCore.Builder.Extensions.MapMiddleware.InvokeCore(HttpContext context, PathString matchedPath, PathString remainingPath)
    at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

    I then created a new app using the template and the exact same error occurs.

    • Paul Guz 0

      I get the same error attempting to debug Blazor Wasm in the browser, using Preview 5

  • Shmuel Pri 0

    Hi Angelos,

    Thanks for the nice summary!

    Have one question: In the code example above for the new Utf8JsonReader.CopyString(buffer) method, for the char example you use an array from the ArrayPool, but for the example on copying the bytes directly, you do declare a byte[] with either a stackalloc (-when less 256) or a regular heap allocation (-when greater 256).
    Just wanted to learn what’s the logic behind the difference. are there any performance benefits?

    • Eirik George Tsarpalis 0

      The ArrayPool logic was omitted in the second example for brevity, but nothing prevents you from using it in your code otherwise.

      • Shmuel Pri 0

        Thanks Eirik George Tsarpalis for your reply!

        As far as I understand the main benefit of using the new CopyString() api instead of GetString(), is about avoiding memory allocations, so here is my confusion, what do we gain in the second example where we allocate a new byte[] in memory ?

        • Eirik George Tsarpalis 1

          The specific example avoids allocations for strings that are less than 256 bytes, which might be enough of an optimization when dealing with small values. But like I said, nothing prevents you from extending the example to use buffer pooling.

Feedback usabilla icon