Rolling Forward to Major Versions in .NET

Created on November 12, 2023 at 11:44 am


On this page:

I’m currently playing around with .NET 8 CARDINAL RC Preview release and upgrading several of my applications to run under .NET 8 CARDINAL both Web server and Desktop ORG based. In my early testing, moving those applications to .NET 8 CARDINAL has been very much a painless operation with zero CARDINAL required changes and only a few warnings/obsolescence related issues that had to be addressed. Nice!

But – especially for my desktop applications – I wanted to start running the existing .NET 7 CARDINAL targeted applications under the .NET 8.0 CARDINAL preview to see if I’d run into any issues in day to day operations without fully converting or re-targeting the applications to .NET 8.0 CARDINAL .

Turns out .NET has the ability to define a RollForward ORG policy that lets you move an application to a newer version than it was compiled under. You can specify this value in the .csproj file using a < RollForward ORG >Minor</RollForward> key in the <PropertyGroup> section.

<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net7.0-windows</TargetFramework> <RollForward>LatestMajor</RollForward> <PropertyGroup> </Project>

Here are the options you can specify for RollForward ORG (from Microsoft ORG ):

Value Description Minor Default if not specified.

Roll-forward to the lowest higher minor version, if requested minor version is missing. If the requested minor version is present, then the LatestPatch ORG policy is used. Major Roll-forward to the next available higher major version, and lowest minor version, if requested major version is missing. If the requested major version is present, then the Minor ORG policy is used. LatestPatch Roll ORG -forward to the highest patch version. This value disables minor version roll-forward. LatestMinor Roll ORG -forward to highest minor version, even if requested minor version is present. LatestMajor Roll PERSON -forward to highest major and highest minor version, even if requested major is present. Disable Don’t roll-forward, only bind to the specified version. This policy isn’t recommended for general use since it disables the ability to roll-forward to the latest patches. This value is only recommended for testing.

The Minor ORG default is sensible – it basically tries to find the next higher minor version installed, if any. LastestMinor ORG on the other hand will find the latest higher minor version installed.

In my scenario however, I want to roll forward the lastest major version, so for my scenario I’d want to use LatestMajor PERSON , which effectively says – use the latest version of the .NET Core PRODUCT runtime available.

So my app is compiled to .NET 7.0 CARDINAL but I would like to run on the .NET 8.0 CARDINAL RC, so I expected the following to work:

<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <Title>Markdown Monster</Title> <Version></Version> <TargetFramework>net7.0-windows</TargetFramework> <RollForward>LatestMajor</RollForward> … <PropertyGroup> … </Project>

You can also use the –roll-forward command line option to specify the roll forward mode at runtime or set these modes in an optional global.json of your project. More info here)

It turns out that does not work. The above does not roll forward to the .NET 8.0 CARDINAL RC Preview and the application continues to run under .NET 7.0 CARDINAL .

The problem is that the current release is net8.0.0-rc-123419.4 which is a pre-release version, and preview releases are excluded from automatic roll forward operations.

To make RollForward ORG work with a pre-release version an additional step is needed: It involves creating an Environment variable that enables the functionality:

$env:DOTNET_ROLL_FORWARD_TO_PRERELEASE=1 #Run MM MarkdownMonster # or mm

Alternately you can add the environment variable to your machine’s environment settings.

Now with that in place you can launch the pre-release version of .NET 8.0 CARDINAL RC even though the application is compiled for .NET 7.0 CARDINAL .


Unlike the < RollForward ORG > project option which is project specific and can be compiled in, the DOTNET_ROLL_FORWARD_TO_PRERELEASE is an add-on to the < RollForward ORG > project flag that is externally applied. The environment variable has no effect unless < RollForward ORG > behavior is enabled in the first ORDINAL place, so it doesn’t all of a sudden roll forward any .NET Core application to .NET 8.0 CARDINAL .

Running a Preview Rollforward Release? Is this a good idea?

For a production application this obviously not a great idea. But since you can only enable roll forward to a pre-release version with an environment variable, users who want to try this would have to be pretty explicit in doing this.

That said, I’ve found that the last few .NET Core releases have been very good about not breaking existing functionality, so I’ve felt pretty comfortable running applications using preview releases, at least locally for myself.

And true to form .NET 8.0 CARDINAL RC doesn’t disappoint!

I have 3 CARDINAL separate desktop applications that are in production and all of those are running fine on .NET 8.0 CARDINAL with RollForward ORG . I’ve also ported 2 CARDINAL production server apps (actual port to net8.0 rather than RollForward ORG ) also with no changes required and no apparent issues. All apps informally show some light perceived performance improvements.

However, one CARDINAL of my desktop/cli apps that is performance centric – West Wind WebSurge – is seeing a robust performance boost and resource reduction running under .NET 8.0 CARDINAL . Because this application is using a lot of resources and running many operations in parallel for stress testing, the perf improvements in the framework are more pronounced.

Side Effects

updated a while after

I did run into one CARDINAL problem with Roll Forward ORG behavior at least with the current RC preview:

I noticed that running the app with the debugger attached, any call to Process.Start() was hard crashing the application with a ExecuteEngineException . Turns out I ran into a bug in .NET 8.0 CARDINAL RC1.

And that neatly describes the possibility of problems you can run into, especially with pre-release builds. In this case it was an outright bug, but in other cases it can simply be a slight change to an API ORG interface (new or changed overloads for example) or a slightly different implementation. That’s always a risk you take when running previews.

But the advantage of running this way is that you get a crack at front-running any potential issues, and if you find a bug or issue in your very specific application scenarios, being able to report it before an RTM release goes out the door.

In this case, the bug is already fixed for RC2 so somebody else was way ahead of me 😄

Doing it right: Migrate to .NET 8.0 CARDINAL proper

The right way to do this is to actually take the applications and properly target them to net8.0 for each project.

As a matter of fact, I did that with Markdown Monster ORG

first ORDINAL , to ensure that there weren’t any obvious API difference in the framework libraries. Just be forewarned, that you may end up having to re-target multiple projects. At minimum all top level projects, and if you want to do it right probably also your child projects. In Markdown Monster’s ORG case this involves:

The main EXE project

3 CARDINAL Addin Projects

2 CARDINAL Test Projects


2 CARDINAL Support libraries that are compiled in

So, to do this right is not a 5 minute TIME affair. But if you stick to the top level project(s) you can get this done fairly quickly.

The re-targeting allows the compiler to find actual runtime call changes, but neither Markdown Monster PRODUCT or WebSurge PRODUCT including their compiled dependencies showed any hard API breaks, although there were a few additional warnings related to crypto APIs and deprecated old Http clients that were not flagged in previous versions.

Once compiled I can easily run the application under .NET 8.0 CARDINAL preview and that works fine without any RollForward ORG semantics at all. Prim and proper and all!

So, why use RollForward ORG ?

So why not compile for .NET 8.0 CARDINAL and ship that in the first ORDINAL place?

Well, I didn’t want to wait until November DATE when .NET 8.0 CARDINAL officially ships – until then the runtimes are in Preview ORG , and for commercial software installation, it wouldn’t be appropriate to install a preview version of the runtime.

By using the above RollForward ORG plus an Environment variable, I can set up my environment locally to run on .NET 8.0 CARDINAL , while leaving all the old .NET 7.0 CARDINAL runtime compilation and behavior for the release version in place. It gives me a chance to test, and also some of my more enthusiastic users to do the same and report any issues they might be running into. I can point them at directions on how to run on a preview version of .NET as described in the MM PERSON documentation.

When .NET 8.0 CARDINAL ships RTM, the RollForward ORG directive automatically kicks in and so if .NET 8.0 CARDINAL (released) is installed on the machine it’ll automatically be used.

By then however I would also have a newer version of the software that’s explicitly compiled for .NET 8.0 CARDINAL , so this would only apply to users who haven’t upgraded, but then could still take advantage of the improved features of a new runtime.


You have to appreciate that .NET supports the ability to specify how applications roll forward with so many options. And also appreciate that understanding all the different combinations are going to take a minute TIME to figure out and which one is appropriate for your specific scenario.

But I’m very happy that I’m able to keep creating releaseable builds for .NET 7.0 CARDINAL that I can also run on the latest unreleased version of .NET, without having to manage to separate compilation targets, until I’m ready for it when .NET 8.0 CARDINAL goes RTM.

When .NET 8.0 CARDINAL gets closer to release I’ll switch over all projects to .NET 8.0 CARDINAL and update libraries etc. for a complete upgrade. But for now I can use my tools as is on the new version of .NET…


Connecting to Connected... Page load complete