Updated on
To get the .NET version from C# code, we read Environment.Version for the runtime version number, or RuntimeInformation.FrameworkDescription for a readable name such as .NET 10.0.10. Both describe the runtime our app is running on right now, which can be newer than the one we built it for.
We usually need this for logging, debugging and bug reports. Beyond the running version, we can also read the target framework, the version another assembly was compiled against, and the SDKs and runtimes installed on the machine.
Which .NET Version Is Our Code Running On?
.NET reports several versions, and they answer different questions. The runtime version belongs to whatever is executing our code right now, the target framework is what we compiled against, and the SDK is the toolchain that built the project.
For the running version, Environment.Version returns a Version object such as 10.0.10, and RuntimeInformation.FrameworkDescription returns a string such as .NET 10.0.10. The Microsoft Learn page for the property says that on .NET Core 3.x and .NET 5+, “the Environment.Version property returns the .NET runtime version number.”
The target framework comes from the TargetFrameworkAttribute that the SDK writes into the assembly at build time, so it never changes after the build. The runtime can be newer. When we run a net8.0 app on .NET 10 with roll forward, it reports .NETCoreApp,Version=v8.0 as its target and 10.0.10 as its runtime.
These methods work on Windows, Linux and macOS.
How to Get the Runtime Version With Environment.Version
The CLR is the virtual machine component that manages the execution of .NET applications. On .NET Framework, Environment.Version reported the CLR version, not the framework version: 4.0.30319.xxxxx on .NET Framework 4 to 4.5.2, and always 4.0.30319.42000 on .NET Framework 4.6 and later.
With the introduction of .NET Core 3.0 and its evolution into the unified .NET platform from .NET 5+ onwards, a single product version is used, which matches both the CLR and the framework description.
To follow along, we create a console app named HowToDetermineDotNetVersionProgramaticallyDemo. Every method in this article goes inside partial class Program, in a file of its own that starts with these usings, and the calling lines go in Program.cs:
using System.Diagnostics;
using System.Reflection;
using System.Runtime;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
partial class Program
{
// the methods from this article go here
}
On .NET 5 and later, we use the Version property of the System.Environment class to get the runtime version. It returns a Version object with the major, minor, build and revision numbers:
public static string GetEnvironmentVersion()
{
return Environment.Version.ToString();
}
Executing this method will give us the .NET runtime version:
Console.WriteLine(GetEnvironmentVersion()); // output: 10.0.10
The number is the exact runtime patch, 10.0.10 here, not the 10.0 our project targets. If we need to compare it, we keep the Version object instead of the string, because Environment.Version >= new Version(10, 0) compares numbers, while comparing strings would put "9.0" after "10.0".
How to Get the Framework Description With RuntimeInformation
The framework description is a string that names the .NET implementation our app runs on, followed by its version. Learn lists the prefixes it can start with: .NET for .NET 5 and later, .NET Core for .NET Core 1.0 to 3.1, .NET Framework and .NET Native. To read it, we use the FrameworkDescription property of the System.Runtime.InteropServices.RuntimeInformation class.
By parsing the result of this property, we can extract the necessary information about the .NET runtime that the application is using:
public static string GetFrameworkDescription()
{
return RuntimeInformation.FrameworkDescription;
}
Calling this method will give us the information about the .NET arrangement under which the application is running:
Console.WriteLine(GetFrameworkDescription()); // output: .NET 10.0.10
var parts = GetFrameworkDescription().Split(" ");
var namePart = parts[0];
Console.WriteLine(namePart); // output: .NET
var versionPart = parts[1];
Console.WriteLine(versionPart); // output: 10.0.10
This split works on .NET 5 and later. On .NET Core 3.1 the string is .NET Core 3.1.5, so parts[1] is Core, not a version. If our code may run on older runtimes, Environment.Version is the safer source for the number.
How to Get the Target Framework
In .NET, the target framework is the version of .NET an application or library is built against. It decides which APIs are available to our code. We set it with a Target Framework Moniker (TFM). The long form of the TFM for .NET 10 is ".NETCoreApp,Version=v10.0" and for .NET Framework 4.8 it is ".NETFramework,Version=v4.8". The short forms are "net10.0" and "net48". The compiler records the long form in a TargetFrameworkAttribute, which we read through reflection:
public static string GetTargetFrameworkName()
{
return Assembly
.GetEntryAssembly()?
.GetCustomAttribute<TargetFrameworkAttribute>()?
.FrameworkName ?? "";
}
When we call the method and write the return value to the console, the version of the target framework will be displayed:
Console.WriteLine(GetTargetFrameworkName()); // output: .NETCoreApp,Version=v10.0
This is the version we built against, and it stays the same whatever runtime ends up executing the app.
How to Determine the .NET Version of Another Assembly
To programmatically get the .NET version of an external assembly, we first have to load it. Then, we find the assembly named System.Runtime from its referenced assemblies. Finally, we call its Version property to get our result:
public static string GetAssemblyDotNetVersion(string assemblyFile)
{
var assembly = Assembly.LoadFrom(assemblyFile);
var references = assembly.GetReferencedAssemblies();
var version = references.FirstOrDefault(x => x.Name == "System.Runtime");
return version?.Version?.ToString() ?? "";
}
When calling the method, we pass the full path to the assembly file. A bare file name resolves against the current working directory, which under dotnet run is not the folder that holds our build output, so we build the path from AppContext.BaseDirectory:
var path = Path.Combine(AppContext.BaseDirectory, "HowToDetermineDotNetVersionProgramaticallyDemo.dll"); Console.WriteLine(GetAssemblyDotNetVersion(path)); // output: 10.0.0.0
For our net10.0 project, the referenced System.Runtime version is 10.0.0.0. It tells us what the assembly was compiled against, the same thing TargetFrameworkAttribute tells us about our own app, and nothing about the runtime it will run on. The number matches the .NET version only for assemblies built for .NET 5 and later: a netcoreapp3.1 build of the same code references System.Runtime 4.2.2.0.
How to Get the .NET Core Version on Older Runtimes
On .NET Core 3.0 and later, Environment.Version already gives us the runtime version. Microsoft’s .NET Core 3.0 compatibility notes explain why older code did something else: in .NET Core 2.2 and earlier, Environment.Version and RuntimeInformation.FrameworkDescription reflected the file version, and only from 3.0 do they reflect the product version. On those older runtimes, the folder name trick below was the way to get a real number.
To determine the version of .NET Core that our assembly is using, we can parse the install path of the System.Runtime.GCSettings class found in the System.Runtime namespace. The install path includes a folder named "Microsoft.NETCore.App", which contains a subfolder with the .NET Core version as its name. For instance, if the assembly is using .NET Core 3.1, the folder name would be a 3.1 patch version such as "3.1.5":
public static string GetNetCoreVersion()
{
var assembly = typeof(GCSettings).Assembly;
var assemblyPath = assembly.Location.Split(['/', '\\'], StringSplitOptions.RemoveEmptyEntries);
var netCoreAppIndex = Array.IndexOf(assemblyPath, "Microsoft.NETCore.App");
if (netCoreAppIndex > 0 && netCoreAppIndex < assemblyPath.Length - 2)
return assemblyPath[netCoreAppIndex + 1];
return "";
}
Using this approach allows us to see the particular .NET Core version executing the application:
Console.WriteLine(GetNetCoreVersion()); // output: 10.0.10
The folder name belongs to the runtime that runs the app, not the one the library was built against, so on .NET 10 we get 10.0.10, the same as Environment.Version. The method also depends on Assembly.Location, which returns an empty string for assemblies bundled into a single-file app. A self-contained single-file build bundles the framework libraries too, so there the method returns an empty string.
How to Get the .NET SDK Versions on Our System
The Process class lets us run the dotnet command-line tool with the --list-sdks option from our own code. The command prints every installed SDK version with its install path, and we parse the resulting string:
public static string GetDotNetSdkVersionsInstalled()
{
using var process = new Process();
process.StartInfo.FileName = "dotnet";
process.StartInfo.Arguments = "--list-sdks";
process.StartInfo.RedirectStandardOutput = true;
process.Start();
var output = process.StandardOutput.ReadToEnd();
process.WaitForExit();
return output;
}
Calling the method gives us one string with a line per SDK:
Console.WriteLine(GetDotNetSdkVersionsInstalled()); // output: // ... // 10.0.301 [C:\Program Files\dotnet\sdk] // 10.0.302 [C:\Program Files\dotnet\sdk]
To get each SDK as a version and a path, we split every line on its first space only. On Windows the install path is C:\Program Files\dotnet\sdk, and it has a space of its own:
public static ICollection<(string version, string installPath)> GetInstalledSdkVersions()
{
return GetDotNetSdkVersionsInstalled()
.Split(Environment.NewLine)
.Select(x => x.Split(' ', 2))
.Where(x => x.Length == 2)
.Select(x => (version: x[0], installPath: x[1]))
.ToArray();
}
Then we print each entry:
foreach (var (sdkVersion, installPath) in GetInstalledSdkVersions())
{
Console.WriteLine($"SDK version: {sdkVersion} Install path: {installPath}");
}
// output:
// ...
// SDK version: 10.0.301 Install path: [C:\Program Files\dotnet\sdk]
// SDK version: 10.0.302 Install path: [C:\Program Files\dotnet\sdk]
The list depends on the machine. To run the same check from a terminal, see how to check which .NET SDKs and runtimes are installed.
How to Determine .NET Runtime Versions on Our System
To list every .NET runtime on the machine, we use the Process class the same way, this time with the --list-runtimes option, and parse the output:
public static string GetDotNetRuntimeVersionsInstalled()
{
using var process = new Process();
process.StartInfo.FileName = "dotnet";
process.StartInfo.Arguments = "--list-runtimes";
process.StartInfo.RedirectStandardOutput = true;
process.Start();
var output = process.StandardOutput.ReadToEnd();
process.WaitForExit();
return output;
}
The method returns one string with a line per runtime:
Console.WriteLine(GetDotNetRuntimeVersionsInstalled()); // output: // ... // Microsoft.NETCore.App 10.0.9 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] // Microsoft.NETCore.App 10.0.10 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] // ...
Each line has a name, a version and a path, so this time we split on the first two spaces:
public static ICollection<(string name, string version, string installPath)>
GetInstalledDotNetRuntimeVersions()
{
return GetDotNetRuntimeVersionsInstalled()
.Split(Environment.NewLine)
.Select(x => x.Split(' ', 3))
.Where(x => x.Length == 3)
.Select(x => (name: x[0], version: x[1], installPath: x[2]))
.ToArray();
}
foreach (var (name, version, installPath) in GetInstalledDotNetRuntimeVersions())
{
Console.WriteLine($"Runtime name: {name} Version: {version} Install path: {installPath}");
}
// output:
// ...
// Runtime name: Microsoft.NETCore.App Version: 10.0.10 Install path: [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
// ...
The list of installed runtimes will vary between various systems. Once you have that version number, the .NET versions and end of support tool shows the release date, end of support date and C# version that go with it.
What Does Environment.Version Return on .NET Core 3.1?
On .NET Core 3.1, Environment.Version returns the real runtime version. We built a small console app for netcoreapp3.1 and ran it on the .NET Core 3.1.5 runtime installed on our machine. It printed 3.1.5 for Environment.Version and .NET Core 3.1.5 for RuntimeInformation.FrameworkDescription.
That behaviour starts with .NET Core 3.0. Microsoft’s compatibility notes for that release say that in .NET Core 2.2 and earlier, both APIs reflected the file version of the runtime, and from 3.0 on they reflect the product version. So code that printed a file version on .NET Core 2.x prints the runtime version on 3.0 and 3.1.
The description changed shape with .NET 5: up to 3.1 it starts with .NET Core, and from .NET 5 on it starts with .NET. If we split it on spaces, the second part on 3.1 is the word Core. Here is what our 3.1 app printed:
3.1.5 .NET Core 3.1.5 .NETCoreApp,Version=v3.1
The third line is the target framework, read from TargetFrameworkAttribute exactly as in the earlier section.
Can We Check the C# Version From Code?
Not from the runtime. The C# language version is a compiler setting, and nothing in Environment or RuntimeInformation reports it. Microsoft Learn suggests a build-time check instead: we put #error version in any C# file and build. The compiler stops with error CS8304 and prints its own version and the language version. With the 10.0.302 SDK and a net10.0 project, ours printed (paths trimmed):
error CS8304: Compiler version: '5.6.0-2.26329.109 (35b593bebfcba58f8e78298cef14c2761f5d86c6)'. Language version: 14.0.
We delete the line once we have the answer, since the build fails while it is there. The default language version follows the target framework, and we can change it with the LangVersion property. To set it once for every project, see setting the C# language version for all projects in a solution.
Conclusion
For most code, Environment.Version and RuntimeInformation.FrameworkDescription are all we need, because they report the runtime executing our code right now. Here is what each method in this article returns on our .NET 10 machine:
| Question | How we ask it | Output on our .NET 10 machine |
|---|---|---|
| Which runtime is running this code? | Environment.Version | 10.0.10 |
| Same, as a readable name | RuntimeInformation.FrameworkDescription | .NET 10.0.10 |
| Which framework did we build against? | TargetFrameworkAttribute.FrameworkName on the entry assembly | .NETCoreApp,Version=v10.0 |
| Which framework did another assembly build against? | Version of its System.Runtime reference | 10.0.0.0 |
| Which SDKs are installed? | dotnet --list-sdks through Process | 10.0.302 [C:\Program Files\dotnet\sdk] |
| Which runtimes are installed? | dotnet --list-runtimes through Process | Microsoft.NETCore.App 10.0.10 [...] |
| Which C# version does the compiler use? | #error version at build time | Language version: 14.0. |
The target framework stays fixed at build time, while the runtime can be newer, so a log can show a net8.0 target running on a .NET 10 runtime, as our roll-forward run did. We use these methods for debugging, testing, compatibility checks and logging, and we can print them at startup so every bug report carries the version it came from.
Tested with .NET 10.0.10 and the 10.0.302 SDK on Windows.
