Skip to content

Commit 119d0a0

Browse files
committed
Retrieve current .Net Framework version from TargetFrameworkAttribute.
1 parent cd50f7b commit 119d0a0

File tree

3 files changed

+96
-45
lines changed

3 files changed

+96
-45
lines changed

src/BenchmarkDotNet/Environments/Runtimes/ClrRuntime.cs

+31-14
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using BenchmarkDotNet.Detectors;
33
using BenchmarkDotNet.Helpers;
44
using BenchmarkDotNet.Jobs;
5-
using BenchmarkDotNet.Portability;
65

76
namespace BenchmarkDotNet.Environments
87
{
@@ -16,6 +15,13 @@ public class ClrRuntime : Runtime, IEquatable<ClrRuntime>
1615
public static readonly ClrRuntime Net48 = new ClrRuntime(RuntimeMoniker.Net48, "net48", ".NET Framework 4.8");
1716
public static readonly ClrRuntime Net481 = new ClrRuntime(RuntimeMoniker.Net481, "net481", ".NET Framework 4.8.1");
1817

18+
// Use a Lazy so that the value will be obtained from the first call which happens on the user's thread.
19+
// When this is called again on a background thread from the BuildInParallel step, it will return the cached result.
20+
#if NET6_0_OR_GREATER
21+
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
22+
#endif
23+
private static readonly Lazy<ClrRuntime> Current = new (RetrieveCurrentVersion, true);
24+
1925
public string Version { get; }
2026

2127
private ClrRuntime(RuntimeMoniker runtimeMoniker, string msBuildMoniker, string displayName, string? version = null)
@@ -50,24 +56,35 @@ internal static ClrRuntime GetCurrentVersion()
5056
throw new NotSupportedException(".NET Framework supports Windows OS only.");
5157
}
5258

59+
return Current.Value;
60+
}
61+
62+
63+
#if NET6_0_OR_GREATER
64+
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
65+
#endif
66+
private static ClrRuntime RetrieveCurrentVersion()
67+
{
5368
// this logic is put to a separate method to avoid any assembly loading issues on non Windows systems
54-
string sdkVersion = FrameworkVersionHelper.GetLatestNetDeveloperPackVersion();
5569

56-
string version = sdkVersion
70+
// Try to determine the Framework version that the executable was compiled for.
71+
string version = FrameworkVersionHelper.GetTargetFrameworkVersion()
72+
// Fallback to the current running Framework version.
73+
?? FrameworkVersionHelper.GetLatestNetDeveloperPackVersion()
5774
?? FrameworkVersionHelper.GetFrameworkReleaseVersion(); // .NET Developer Pack is not installed
5875

59-
switch (version)
76+
return version switch
6077
{
61-
case "4.6.1": return Net461;
62-
case "4.6.2": return Net462;
63-
case "4.7": return Net47;
64-
case "4.7.1": return Net471;
65-
case "4.7.2": return Net472;
66-
case "4.8": return Net48;
67-
case "4.8.1": return Net481;
68-
default: // unlikely to happen but theoretically possible
69-
return new ClrRuntime(RuntimeMoniker.NotRecognized, $"net{version.Replace(".", null)}", $".NET Framework {version}");
70-
}
78+
"4.6.1" => Net461,
79+
"4.6.2" => Net462,
80+
"4.7" => Net47,
81+
"4.7.1" => Net471,
82+
"4.7.2" => Net472,
83+
"4.8" => Net48,
84+
"4.8.1" => Net481,
85+
// unlikely to happen but theoretically possible
86+
_ => new ClrRuntime(RuntimeMoniker.NotRecognized, $"net{version.Replace(".", null)}", $".NET Framework {version}"),
87+
};
7188
}
7289
}
7390
}

src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs

+64-15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
24
using System.IO;
35
using System.Linq;
6+
using System.Reflection;
7+
using System.Runtime.Versioning;
48
using Microsoft.Win32;
59

610
namespace BenchmarkDotNet.Helpers
@@ -10,15 +14,62 @@ internal static class FrameworkVersionHelper
1014
// magic numbers come from https://docs.microsoft.com/en-us/dotnet/framework/migration-guide/how-to-determine-which-versions-are-installed
1115
// should be ordered by release number
1216
private static readonly (int minReleaseNumber, string version)[] FrameworkVersions =
13-
{
17+
[
1418
(533320, "4.8.1"), // value taken from Windows 11 arm64 insider build
1519
(528040, "4.8"),
1620
(461808, "4.7.2"),
1721
(461308, "4.7.1"),
1822
(460798, "4.7"),
1923
(394802, "4.6.2"),
2024
(394254, "4.6.1")
21-
};
25+
];
26+
27+
internal static string? GetTargetFrameworkVersion()
28+
{
29+
// Search assemblies until we find a TargetFrameworkAttribute with a supported Framework version.
30+
// We don't search all assemblies, only the entry assembly and callers.
31+
foreach (var assembly in EnumerateAssemblies())
32+
{
33+
foreach (var attribute in assembly.GetCustomAttributes<TargetFrameworkAttribute>())
34+
{
35+
switch (attribute.FrameworkName)
36+
{
37+
case ".NETFramework,Version=v4.6.1": return "4.6.1";
38+
case ".NETFramework,Version=v4.6.2": return "4.6.2";
39+
case ".NETFramework,Version=v4.7": return "4.7";
40+
case ".NETFramework,Version=v4.7.1": return "4.7.1";
41+
case ".NETFramework,Version=v4.7.2": return "4.7.2";
42+
case ".NETFramework,Version=v4.8": return "4.8";
43+
case ".NETFramework,Version=v4.8.1": return "4.8.1";
44+
}
45+
}
46+
}
47+
48+
return null;
49+
50+
static IEnumerable<Assembly> EnumerateAssemblies()
51+
{
52+
var entryAssembly = Assembly.GetEntryAssembly();
53+
// Assembly.GetEntryAssembly() returns null in unit test frameworks.
54+
if (entryAssembly != null)
55+
{
56+
yield return entryAssembly;
57+
}
58+
// Search calling assemblies.
59+
var stacktrace = new StackTrace(false);
60+
var currentAssembly = stacktrace.GetFrame(0).GetMethod().ReflectedType.Assembly;
61+
for (int i = 1; i < stacktrace.FrameCount; i++)
62+
{
63+
StackFrame frame = stacktrace.GetFrame(i);
64+
var assembly = frame.GetMethod().ReflectedType.Assembly;
65+
if (assembly != currentAssembly)
66+
{
67+
currentAssembly = assembly;
68+
yield return assembly;
69+
}
70+
}
71+
}
72+
}
2273

2374
internal static string GetFrameworkDescription()
2475
{
@@ -57,30 +108,28 @@ internal static string MapToReleaseVersion(string servicingVersion)
57108

58109

59110
#if NET6_0_OR_GREATER
60-
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
111+
[SupportedOSPlatform("windows")]
61112
#endif
62113
private static int? GetReleaseNumberFromWindowsRegistry()
63114
{
64-
using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32))
65-
using (var ndpKey = baseKey.OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\"))
66-
{
67-
if (ndpKey == null)
68-
return null;
69-
return Convert.ToInt32(ndpKey.GetValue("Release"));
70-
}
115+
using var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32);
116+
using var ndpKey = baseKey.OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\");
117+
if (ndpKey == null)
118+
return null;
119+
return Convert.ToInt32(ndpKey.GetValue("Release"));
71120
}
72121

73122
#if NET6_0_OR_GREATER
74-
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
123+
[SupportedOSPlatform("windows")]
75124
#endif
76-
internal static string GetLatestNetDeveloperPackVersion()
125+
internal static string? GetLatestNetDeveloperPackVersion()
77126
{
78-
if (!(GetReleaseNumberFromWindowsRegistry() is int releaseNumber))
127+
if (GetReleaseNumberFromWindowsRegistry() is not int releaseNumber)
79128
return null;
80129

81130
return FrameworkVersions
82-
.FirstOrDefault(v => releaseNumber >= v.minReleaseNumber && IsDeveloperPackInstalled(v.version))
83-
.version;
131+
.FirstOrDefault(v => releaseNumber >= v.minReleaseNumber && IsDeveloperPackInstalled(v.version))
132+
.version;
84133
}
85134

86135
// Reference Assemblies exists when Developer Pack is installed

tests/BenchmarkDotNet.IntegrationTests.ManualRunning.MultipleFrameworks/MultipleFrameworksTest.cs

+1-16
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
using BenchmarkDotNet.Configs;
44
using BenchmarkDotNet.Extensions;
55
using BenchmarkDotNet.Jobs;
6-
using BenchmarkDotNet.Portability;
76
using Xunit;
87

98
namespace BenchmarkDotNet.IntegrationTests.ManualRunning
@@ -19,20 +18,6 @@ public class MultipleFrameworksTest : BenchmarkTestExecutor
1918
[InlineData(RuntimeMoniker.Net80)]
2019
public void EachFrameworkIsRebuilt(RuntimeMoniker runtime)
2120
{
22-
#if NET462
23-
// We cannot detect what target framework version the host was compiled for on full Framework,
24-
// which causes the RoslynToolchain to be used instead of CsProjClassicNetToolchain when the host is full Framework
25-
// (because full Framework always uses the version that's installed on the machine, unlike Core),
26-
// which means if the machine has net48 installed (not net481), the net461 host with net48 runtime moniker
27-
// will not be recompiled, causing the test to fail.
28-
29-
// If we ever change the default toolchain to CsProjClassicNetToolchain instead of RoslynToolchain, we can remove this check.
30-
if (runtime == RuntimeMoniker.Net48)
31-
{
32-
// XUnit doesn't provide Assert.Skip API yet.
33-
return;
34-
}
35-
#endif
3621
var config = ManualConfig.CreateEmpty().AddJob(Job.Dry.WithRuntime(runtime.GetRuntime()).WithEnvironmentVariable(TfmEnvVarName, runtime.ToString()));
3722
CanExecute<ValuePerTfm>(config);
3823
}
@@ -57,7 +42,7 @@ public void ThrowWhenWrong()
5742
{
5843
if (Environment.GetEnvironmentVariable(TfmEnvVarName) != moniker.ToString())
5944
{
60-
throw new InvalidOperationException($"Has not been recompiled, the value was {Environment.GetEnvironmentVariable(TfmEnvVarName)}");
45+
throw new InvalidOperationException($"Has not been recompiled, the value was {moniker}, expected {Environment.GetEnvironmentVariable(TfmEnvVarName)}");
6146
}
6247
}
6348
}

0 commit comments

Comments
 (0)