From 4c08ef5df3862ad8c75e71471d6e6ae8ce6b555c Mon Sep 17 00:00:00 2001 From: Paul Medynski <31868385+paulmedynski@users.noreply.github.com> Date: Fri, 1 May 2026 12:35:48 -0300 Subject: [PATCH 1/4] Consolidate OS detection to OsConstants - Create new OsConstants class with IsWindows, IsLinux, IsMacOS, IsFreeBSD, and IsUnix fields - Add comprehensive XML documentation and explain why static constructor is preferred over [ModuleInitializer] (CA2255 rule) - Replace all RuntimeInformation.IsOSPlatform() calls with OsConstants fields in: - TdsParserStaticMethods.cs - UserAgent.cs - AdapterUtil.cs (now forwards to OsConstants) - Replace Environment.OSVersion.Platform checks with OsConstants in: - SqlColumnEncryptionCertificateStoreProvider.cs (3 instances) - SqlUtil.cs (3 instances) - Replace OperatingSystem static methods with OsConstants in: - LocalAppContextSwitches.cs (uses IsUnix for clearer semantics) - TdsParser.cs - Replace all ADP.IsWindows usage with OsConstants in: - SqlFileStream.cs - SqlColumnEncryptionCngProvider.cs (4 instances) - SqlColumnEncryptionCspProvider.cs (4 instances) - Remove unused AdapterUtil.IsWindows property Benefits: - Single point of control for OS detection - Improved JIT optimization through static readonly cached flags - Clearer semantics with IsUnix flag for Unix-like platforms - Consistent API surface across the codebase --- .../src/Microsoft/Data/Common/AdapterUtil.cs | 20 +---- .../Data/SqlClient/LocalAppContextSwitches.cs | 2 +- .../Microsoft/Data/SqlClient/OsConstants.cs | 82 +++++++++++++++++++ ...olumnEncryptionCertificateStoreProvider.cs | 7 +- .../SqlColumnEncryptionCngProvider.cs | 8 +- .../SqlColumnEncryptionCspProvider.cs | 8 +- .../src/Microsoft/Data/SqlClient/SqlUtil.cs | 6 +- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 2 +- .../Data/SqlClient/TdsParserStaticMethods.cs | 3 +- .../src/Microsoft/Data/SqlClient/UserAgent.cs | 13 ++- .../Microsoft/Data/SqlTypes/SqlFileStream.cs | 2 +- 11 files changed, 108 insertions(+), 45 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/OsConstants.cs diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs index 568592072d..30c4051cc1 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs @@ -13,13 +13,13 @@ using System.IO; using System.Runtime.CompilerServices; using System.Security; +using Microsoft.Data.SqlClient; using System.Security.Authentication; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Transactions; using Microsoft.Data.Common.ConnectionString; -using Microsoft.Data.SqlClient; using Microsoft.Data.SqlClient.Connection; using Microsoft.SqlServer.Server; using Microsoft.Win32; @@ -65,22 +65,6 @@ internal static partial class ADP /// internal const int MaxBufferAccessTokenExpiry = 600; - /// - /// This member returns true if the current OS platform is Windows. - /// - /// - /// This is a const on .NET Framework, and a property on .NET Core, because of differing API availability and JIT requirements. - /// .NET Framework will perform basic dead branch elimination when a const value is encountered, while .NET Core can trim Windows-specific - /// code when published to non-Windows platforms. - /// .NET Core's trimming is very limited though, so this must be used inline within methods to throw PlatformNotSupportedException, - /// rather than in a throw helper. - /// - #if NETFRAMEWORK - public const bool IsWindows = true; - #else - public static bool IsWindows => OperatingSystem.IsWindows(); - #endif - #region UDT #if NETFRAMEWORK @@ -441,7 +425,7 @@ internal static ArgumentOutOfRangeException InvalidCommandBehavior(CommandBehavi internal static object LocalMachineRegistryValue(string subkey, string queryvalue) { #if NET - if (!IsWindows) + if (OsConstants.IsUnix) { // No registry in non-Windows environments return null; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs index d951bb5d30..49fa9f1eba 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs @@ -635,7 +635,7 @@ public static bool UseManagedNetworking return s_useManagedNetworking == SwitchValue.True; } - if (!OperatingSystem.IsWindows()) + if (OsConstants.IsUnix) { s_useManagedNetworking = SwitchValue.True; return true; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/OsConstants.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/OsConstants.cs new file mode 100644 index 0000000000..6c0a7c7e39 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/OsConstants.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.InteropServices; + +namespace Microsoft.Data.SqlClient; + +/// +/// Provides platform detection flags for OS-specific code paths. +/// +/// +/// These constants are computed at runtime and cached as static readonly fields. This design +/// allows the JIT compiler to elide branches in hot paths based on whether the OS flags are known +/// constants at JIT compilation time. +/// +internal static class OsConstants +{ + /// + /// Gets a value indicating whether the runtime is executing on Windows. + /// + internal static readonly bool IsWindows; + + /// + /// Gets a value indicating whether the runtime is executing on Linux. + /// + internal static readonly bool IsLinux; + + /// + /// Gets a value indicating whether the runtime is executing on macOS. + /// + internal static readonly bool IsMacOS; + +#if NET + /// + /// Gets a value indicating whether the runtime is executing on FreeBSD. + /// + /// + /// FreeBSD support is only available in .NET 5+ and later. This field will be + /// false on .NET Framework or if the runtime does not support FreeBSD detection. + /// + internal static readonly bool IsFreeBSD; +#endif + + /// + /// Gets a value indicating whether the runtime is executing on a Unix-like operating system. + /// + /// + /// This is true for Linux, macOS, FreeBSD, and other Unix-like platforms. + /// It is false only on Windows. + /// + internal static readonly bool IsUnix; + + /// + /// Initializes platform detection flags by querying . + /// + /// + /// We use a static constructor instead of a module initializer ([ModuleInitializer]) to avoid + /// the CA2255 security concern. Module initializers can be problematic because: 1. They run in + /// an unpredictable order relative to other initialization code. 2. They run before the app + /// initialization sequence, potentially before security policies are set. 3. They can + /// complicate debugging and profiling. + /// + /// Using a static constructor ensures initialization happens in a well-defined, type-safe + /// manner that is compatible with the CLR's type loading guarantees. + /// + /// The trade-off is that the OS flags won't be initialized until the OsConstants type is first + /// accessed, which may cause a slight delay in a hot path, but only once. + /// + static OsConstants() + { + IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + IsLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + IsMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX); +#if NET + IsFreeBSD = RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD); + IsUnix = IsLinux || IsMacOS || IsFreeBSD; +#else + IsUnix = IsLinux || IsMacOS; +#endif + } +} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCertificateStoreProvider.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCertificateStoreProvider.cs index b02dd24af1..f0438cd162 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCertificateStoreProvider.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCertificateStoreProvider.cs @@ -57,7 +57,7 @@ public class SqlColumnEncryptionCertificateStoreProvider : SqlColumnEncryptionKe /// Gets a string array containing valid certificate locations. /// private static string[] ValidCertificateLocations => - Environment.OSVersion.Platform == PlatformID.Win32NT + OsConstants.IsWindows ? [CertLocationLocalMachine, CertLocationCurrentUser] : [CertLocationCurrentUser]; @@ -158,14 +158,13 @@ private static RSA GetCertificatePrivateKeyByPath(string keyPath, bool isSystemO } // Extract the store location where the cert is stored - if (storeLocationSpan.IsEmpty - && Environment.OSVersion.Platform == PlatformID.Win32NT) + if (storeLocationSpan.IsEmpty && OsConstants.IsWindows) { // Default to Local Machine on Windows. Non-Windows platforms only support CurrentUser storeLocation = StoreLocation.LocalMachine; } else if (storeLocationSpan.Equals(CertLocationLocalMachine.AsSpan(), StringComparison.OrdinalIgnoreCase) - && Environment.OSVersion.Platform == PlatformID.Win32NT) + && OsConstants.IsWindows) { storeLocation = StoreLocation.LocalMachine; } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCngProvider.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCngProvider.cs index fc65808cf1..d585e00cdd 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCngProvider.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCngProvider.cs @@ -149,7 +149,7 @@ private static void GetCngProviderAndKeyId(string keyPath, bool isSystemOp, out /// public override byte[] DecryptColumnEncryptionKey(string? masterKeyPath, string? encryptionAlgorithm, byte[]? encryptedColumnEncryptionKey) { - if (!ADP.IsWindows) + if (OsConstants.IsUnix) { throw new PlatformNotSupportedException(); } @@ -180,7 +180,7 @@ public override byte[] DecryptColumnEncryptionKey(string? masterKeyPath, string? /// public override byte[] EncryptColumnEncryptionKey(string? masterKeyPath, string? encryptionAlgorithm, byte[]? columnEncryptionKey) { - if (!ADP.IsWindows) + if (OsConstants.IsUnix) { throw new PlatformNotSupportedException(); } @@ -211,7 +211,7 @@ public override byte[] EncryptColumnEncryptionKey(string? masterKeyPath, string? /// public override byte[] SignColumnMasterKeyMetadata(string? masterKeyPath, bool allowEnclaveComputations) { - throw ADP.IsWindows + throw OsConstants.IsWindows ? new NotSupportedException() : new PlatformNotSupportedException(); } @@ -219,7 +219,7 @@ public override byte[] SignColumnMasterKeyMetadata(string? masterKeyPath, bool a /// public override bool VerifyColumnMasterKeyMetadata(string? masterKeyPath, bool allowEnclaveComputations, byte[]? signature) { - throw ADP.IsWindows + throw OsConstants.IsWindows ? new NotSupportedException() : new PlatformNotSupportedException(); } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCspProvider.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCspProvider.cs index 73ee585dfa..1ba0d47f63 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCspProvider.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCspProvider.cs @@ -151,7 +151,7 @@ private static int GetProviderType(string providerName, string keyPath, bool isS /// public override byte[] DecryptColumnEncryptionKey(string? masterKeyPath, string? encryptionAlgorithm, byte[]? encryptedColumnEncryptionKey) { - if (!ADP.IsWindows) + if (OsConstants.IsUnix) { throw new PlatformNotSupportedException(); } @@ -182,7 +182,7 @@ public override byte[] DecryptColumnEncryptionKey(string? masterKeyPath, string? /// public override byte[] EncryptColumnEncryptionKey(string? masterKeyPath, string? encryptionAlgorithm, byte[]? columnEncryptionKey) { - if (!ADP.IsWindows) + if (OsConstants.IsUnix) { throw new PlatformNotSupportedException(); } @@ -213,7 +213,7 @@ public override byte[] EncryptColumnEncryptionKey(string? masterKeyPath, string? /// public override byte[] SignColumnMasterKeyMetadata(string? masterKeyPath, bool allowEnclaveComputations) { - throw ADP.IsWindows + throw OsConstants.IsWindows ? new NotSupportedException() : new PlatformNotSupportedException(); } @@ -221,7 +221,7 @@ public override byte[] SignColumnMasterKeyMetadata(string? masterKeyPath, bool a /// public override bool VerifyColumnMasterKeyMetadata(string? masterKeyPath, bool allowEnclaveComputations, byte[]? signature) { - throw ADP.IsWindows + throw OsConstants.IsWindows ? new NotSupportedException() : new PlatformNotSupportedException(); } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs index 678697d221..e73a69fd04 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs @@ -1141,7 +1141,7 @@ internal static Exception LargeCertificatePathLength(int actualLength, int maxLe internal static Exception NullCertificatePath(string[] validLocations, bool isSystemOp) { - if (Environment.OSVersion.Platform == PlatformID.Win32NT) + if (OsConstants.IsWindows) { Debug.Assert(validLocations.Length == 2); if (isSystemOp) @@ -1193,7 +1193,7 @@ internal static Exception NullCngKeyPath(bool isSystemOp) internal static Exception InvalidCertificatePath(string actualCertificatePath, string[] validLocations, bool isSystemOp) { - if (Environment.OSVersion.Platform == PlatformID.Win32NT) + if (OsConstants.IsWindows) { Debug.Assert(validLocations.Length == 2); if (isSystemOp) @@ -1329,7 +1329,7 @@ internal static Exception InvalidCngKey(string masterKeyPath, string cngProvider internal static Exception InvalidCertificateLocation(string certificateLocation, string certificatePath, string[] validLocations, bool isSystemOp) { - if (Environment.OSVersion.Platform == PlatformID.Win32NT) + if (OsConstants.IsWindows) { Debug.Assert(validLocations.Length == 2); if (isSystemOp) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs index c3f96b077a..660bfdccde 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -979,7 +979,7 @@ private void EnableSsl(uint info, SqlConnectionEncryptOption encrypt, bool integ // Channel Bindings as part of the Windows Authentication context build (SSL handshake must complete // before calling SNISecGenClientContext). #if NET - if (OperatingSystem.IsWindows()) + if (OsConstants.IsWindows) #endif { error = _physicalStateObj.WaitForSSLHandShakeToComplete(out protocol); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStaticMethods.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStaticMethods.cs index a384095254..2dcd38c7d9 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStaticMethods.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStaticMethods.cs @@ -4,7 +4,6 @@ using System; using System.Globalization; -using System.Runtime.InteropServices; using Microsoft.Data.Common; namespace Microsoft.Data.SqlClient @@ -173,7 +172,7 @@ static internal byte[] GetNetworkPhysicalAddressForTdsLoginOnly() byte[] nicAddress = null; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (OsConstants.IsWindows) { // NIC address is stored in NetworkAddress key. However, if NetworkAddressLocal key // has a value that is not zero, then we cannot use the NetworkAddress key and must diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent.cs index a8fcf7ca88..63cd220d6c 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent.cs @@ -120,25 +120,24 @@ static UserAgent() // specific values. // string osType = Unknown; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (OsConstants.IsWindows) { osType = Windows; } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + else if (OsConstants.IsLinux) { osType = Linux; } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + else if (OsConstants.IsMacOS) { osType = macOS; } - // The FreeBSD platform doesn't exist in .NET Framework at all. - #if NET - else if (RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD)) +#if NET + else if (OsConstants.IsFreeBSD) { osType = FreeBSD; } - #endif +#endif // Build it! Value = Build( diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlFileStream.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlFileStream.cs index 4c48890fc4..8278d4c23c 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlFileStream.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlFileStream.cs @@ -102,7 +102,7 @@ public SqlFileStream( FileOptions options, long allocationSize) { - if (!ADP.IsWindows) + if (OsConstants.IsUnix) { throw new PlatformNotSupportedException(Strings.SqlFileStream_NotSupported); } From 8f5e2d0893ea3cd07ba2e6547582ef4bb93fb1df Mon Sep 17 00:00:00 2001 From: Paul Medynski <31868385+paulmedynski@users.noreply.github.com> Date: Fri, 1 May 2026 12:50:42 -0300 Subject: [PATCH 2/4] Address Copilot OS guard feedback - Remove OsConstants.IsUnix to avoid non-Windows ambiguity - Update Windows-only guards in SqlFileStream, CNG, CSP, and switch logic - Keep explicit OS flags (Windows/Linux/macOS/FreeBSD) --- .../src/Microsoft/Data/Common/AdapterUtil.cs | 4 ++-- .../Data/SqlClient/LocalAppContextSwitches.cs | 2 +- .../Microsoft/Data/SqlClient/OsConstants.cs | 20 ++++--------------- .../SqlColumnEncryptionCngProvider.cs | 4 ++-- .../SqlColumnEncryptionCspProvider.cs | 4 ++-- .../Microsoft/Data/SqlTypes/SqlFileStream.cs | 2 +- 6 files changed, 12 insertions(+), 24 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs index 30c4051cc1..ad58f4072e 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs @@ -13,13 +13,13 @@ using System.IO; using System.Runtime.CompilerServices; using System.Security; -using Microsoft.Data.SqlClient; using System.Security.Authentication; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Transactions; using Microsoft.Data.Common.ConnectionString; +using Microsoft.Data.SqlClient; using Microsoft.Data.SqlClient.Connection; using Microsoft.SqlServer.Server; using Microsoft.Win32; @@ -425,7 +425,7 @@ internal static ArgumentOutOfRangeException InvalidCommandBehavior(CommandBehavi internal static object LocalMachineRegistryValue(string subkey, string queryvalue) { #if NET - if (OsConstants.IsUnix) + if (!OsConstants.IsWindows) { // No registry in non-Windows environments return null; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs index 49fa9f1eba..85f8c87937 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs @@ -635,7 +635,7 @@ public static bool UseManagedNetworking return s_useManagedNetworking == SwitchValue.True; } - if (OsConstants.IsUnix) + if (!OsConstants.IsWindows) { s_useManagedNetworking = SwitchValue.True; return true; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/OsConstants.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/OsConstants.cs index 6c0a7c7e39..09f2c27960 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/OsConstants.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/OsConstants.cs @@ -31,7 +31,7 @@ internal static class OsConstants /// internal static readonly bool IsMacOS; -#if NET + #if NET /// /// Gets a value indicating whether the runtime is executing on FreeBSD. /// @@ -40,16 +40,7 @@ internal static class OsConstants /// false on .NET Framework or if the runtime does not support FreeBSD detection. /// internal static readonly bool IsFreeBSD; -#endif - - /// - /// Gets a value indicating whether the runtime is executing on a Unix-like operating system. - /// - /// - /// This is true for Linux, macOS, FreeBSD, and other Unix-like platforms. - /// It is false only on Windows. - /// - internal static readonly bool IsUnix; + #endif /// /// Initializes platform detection flags by querying . @@ -72,11 +63,8 @@ static OsConstants() IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); IsLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); IsMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX); -#if NET + #if NET IsFreeBSD = RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD); - IsUnix = IsLinux || IsMacOS || IsFreeBSD; -#else - IsUnix = IsLinux || IsMacOS; -#endif + #endif } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCngProvider.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCngProvider.cs index d585e00cdd..4d0d79b750 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCngProvider.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCngProvider.cs @@ -149,7 +149,7 @@ private static void GetCngProviderAndKeyId(string keyPath, bool isSystemOp, out /// public override byte[] DecryptColumnEncryptionKey(string? masterKeyPath, string? encryptionAlgorithm, byte[]? encryptedColumnEncryptionKey) { - if (OsConstants.IsUnix) + if (!OsConstants.IsWindows) { throw new PlatformNotSupportedException(); } @@ -180,7 +180,7 @@ public override byte[] DecryptColumnEncryptionKey(string? masterKeyPath, string? /// public override byte[] EncryptColumnEncryptionKey(string? masterKeyPath, string? encryptionAlgorithm, byte[]? columnEncryptionKey) { - if (OsConstants.IsUnix) + if (!OsConstants.IsWindows) { throw new PlatformNotSupportedException(); } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCspProvider.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCspProvider.cs index 1ba0d47f63..dc956d9c71 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCspProvider.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCspProvider.cs @@ -151,7 +151,7 @@ private static int GetProviderType(string providerName, string keyPath, bool isS /// public override byte[] DecryptColumnEncryptionKey(string? masterKeyPath, string? encryptionAlgorithm, byte[]? encryptedColumnEncryptionKey) { - if (OsConstants.IsUnix) + if (!OsConstants.IsWindows) { throw new PlatformNotSupportedException(); } @@ -182,7 +182,7 @@ public override byte[] DecryptColumnEncryptionKey(string? masterKeyPath, string? /// public override byte[] EncryptColumnEncryptionKey(string? masterKeyPath, string? encryptionAlgorithm, byte[]? columnEncryptionKey) { - if (OsConstants.IsUnix) + if (!OsConstants.IsWindows) { throw new PlatformNotSupportedException(); } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlFileStream.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlFileStream.cs index 8278d4c23c..6059705910 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlFileStream.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlFileStream.cs @@ -102,7 +102,7 @@ public SqlFileStream( FileOptions options, long allocationSize) { - if (OsConstants.IsUnix) + if (!OsConstants.IsWindows) { throw new PlatformNotSupportedException(Strings.SqlFileStream_NotSupported); } From 49c3f7218c6a78879049b48ab0694de4fc8d89d4 Mon Sep 17 00:00:00 2001 From: Paul Medynski <31868385+paulmedynski@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:32:10 -0300 Subject: [PATCH 3/4] Expose OsConstants flags as intrinsic properties for IL trimming Replace the static readonly fields and static constructor in OsConstants with properties that directly return RuntimeInformation.IsOSPlatform(...). The IL trimmer cannot analyze a static constructor, so the cached-field design left OS-specific code (including Windows-only native dependencies) in apps published for other platforms. Returning the intrinsic directly lets the trimmer fold each guard to a constant and drop the dead branch. Also document why the type exists (brevity and encapsulation), the inline-guard requirement for trimming, and reconcile the stale remark about JIT branch elision. Addresses PR #4255 review feedback from @edwardneal. --- .../Microsoft/Data/SqlClient/OsConstants.cs | 90 ++++++++++++------- 1 file changed, 56 insertions(+), 34 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/OsConstants.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/OsConstants.cs index 09f2c27960..71f2a3aef0 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/OsConstants.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/OsConstants.cs @@ -10,61 +10,83 @@ namespace Microsoft.Data.SqlClient; /// Provides platform detection flags for OS-specific code paths. /// /// -/// These constants are computed at runtime and cached as static readonly fields. This design -/// allows the JIT compiler to elide branches in hot paths based on whether the OS flags are known -/// constants at JIT compilation time. +/// This type exists to keep OS detection terse and in one place: callers write +/// OsConstants.IsWindows instead of repeating RuntimeInformation.IsOSPlatform(...) +/// throughout the codebase. Centralizing it also gives us a single seam to adjust if a specific +/// OS ever needs special handling (for example, a different detection mechanism, a finer-grained +/// flag, or an override for testing) without touching every call site. +/// +/// +/// Each flag is exposed as a property that directly returns the result of +/// . This is deliberate: the IL trimmer (and the JIT) +/// recognize these calls as intrinsics and can substitute a constant value for them at +/// publish/compile time. Caching the results in static fields (for example, via a static +/// constructor) would defeat this — the trimmer cannot analyze a static constructor, so it would +/// be unable to prove which branches are dead and would keep OS-specific code (including +/// Windows-only native dependencies) in apps published for other platforms. +/// +/// +/// +/// IL trimming note: when a downstream app is published for a specific OS (a RID-specific publish), +/// the IL trimmer substitutes the underlying / +/// OperatingSystem.Is* checks with constant true/false for the target OS and +/// then removes the dead branch (along with everything it transitively references, such as +/// Windows-only native entry points). This is what lets a single cross-platform assembly trim +/// away the OS-specific code that does not apply to the published target. +/// +/// +/// For this to work, each guard must be used inline in the same method as the OS-specific +/// code it gates. The trimmer's constant folding is shallow: it reasons within the method that +/// contains the guard, so a guard hidden behind a helper traps the constant inside that helper and +/// leaves the protected code reachable (and therefore not trimmed). +/// +/// +/// Do — guard inline, so the trimmer can drop the branch and its dependencies: +/// +/// if (OsConstants.IsWindows) +/// { +/// WindowsOnlyNativeCall(); // becomes `if (false) { ... }` off-Windows, then removed +/// } +/// +/// +/// +/// Don't — hide the guard in a throw helper; the trimmer cannot prove the call below is dead: +/// +/// static void ThrowIfNotWindows() +/// { +/// if (!OsConstants.IsWindows) throw new PlatformNotSupportedException(); +/// } +/// // caller: +/// ThrowIfNotWindows(); +/// WindowsOnlyNativeCall(); // still reachable to the trimmer; kept even off-Windows +/// +/// /// internal static class OsConstants { /// /// Gets a value indicating whether the runtime is executing on Windows. /// - internal static readonly bool IsWindows; + internal static bool IsWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); /// /// Gets a value indicating whether the runtime is executing on Linux. /// - internal static readonly bool IsLinux; + internal static bool IsLinux => RuntimeInformation.IsOSPlatform(OSPlatform.Linux); /// /// Gets a value indicating whether the runtime is executing on macOS. /// - internal static readonly bool IsMacOS; + internal static bool IsMacOS => RuntimeInformation.IsOSPlatform(OSPlatform.OSX); #if NET /// /// Gets a value indicating whether the runtime is executing on FreeBSD. /// /// - /// FreeBSD support is only available in .NET 5+ and later. This field will be + /// FreeBSD support is only available in .NET 5+ and later. This property will be /// false on .NET Framework or if the runtime does not support FreeBSD detection. /// - internal static readonly bool IsFreeBSD; + internal static bool IsFreeBSD => RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD); #endif - - /// - /// Initializes platform detection flags by querying . - /// - /// - /// We use a static constructor instead of a module initializer ([ModuleInitializer]) to avoid - /// the CA2255 security concern. Module initializers can be problematic because: 1. They run in - /// an unpredictable order relative to other initialization code. 2. They run before the app - /// initialization sequence, potentially before security policies are set. 3. They can - /// complicate debugging and profiling. - /// - /// Using a static constructor ensures initialization happens in a well-defined, type-safe - /// manner that is compatible with the CLR's type loading guarantees. - /// - /// The trade-off is that the OS flags won't be initialized until the OsConstants type is first - /// accessed, which may cause a slight delay in a hot path, but only once. - /// - static OsConstants() - { - IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - IsLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); - IsMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX); - #if NET - IsFreeBSD = RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD); - #endif - } } From 6d509a8d18e1e89776677f7e5cb2ce2f05e628d2 Mon Sep 17 00:00:00 2001 From: Paul Medynski <31868385+paulmedynski@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:54:39 -0300 Subject: [PATCH 4/4] Use OperatingSystem.Is*() on .NET for JIT-intrinsic OS checks On modern .NET, OperatingSystem.IsWindows()/IsLinux()/IsMacOS()/IsFreeBSD() are JIT intrinsics that get constant-folded (and are recognized by the IL trimmer), so switch the OsConstants properties to them under #if NET. .NET Framework keeps RuntimeInformation.IsOSPlatform(...) since the OperatingSystem platform-check helpers do not exist there. This addresses the automated reviewer feedback that RuntimeInformation .IsOSPlatform(...) misses the JIT constant-folding optimization on net8/net9, while preserving the trimming correctness from the prior change. Addresses PR #4255 review feedback. --- .../Microsoft/Data/SqlClient/OsConstants.cs | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/OsConstants.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/OsConstants.cs index 71f2a3aef0..b496141693 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/OsConstants.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/OsConstants.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Runtime.InteropServices; namespace Microsoft.Data.SqlClient; @@ -13,16 +14,18 @@ namespace Microsoft.Data.SqlClient; /// This type exists to keep OS detection terse and in one place: callers write /// OsConstants.IsWindows instead of repeating RuntimeInformation.IsOSPlatform(...) /// throughout the codebase. Centralizing it also gives us a single seam to adjust if a specific -/// OS ever needs special handling (for example, a different detection mechanism, a finer-grained +/// OS needs special handling (for example, a different detection mechanism, a finer-grained /// flag, or an override for testing) without touching every call site. /// /// -/// Each flag is exposed as a property that directly returns the result of -/// . This is deliberate: the IL trimmer (and the JIT) -/// recognize these calls as intrinsics and can substitute a constant value for them at -/// publish/compile time. Caching the results in static fields (for example, via a static -/// constructor) would defeat this — the trimmer cannot analyze a static constructor, so it would -/// be unable to prove which branches are dead and would keep OS-specific code (including +/// Each flag is exposed as a property that directly returns an OS check. On .NET, that check is +/// OperatingSystem.Is*(), which the JIT treats as an intrinsic and constant-folds (and which +/// the IL trimmer also recognizes). On .NET Framework — where the +/// platform-check helpers do not exist — the check falls back to +/// . Either way, returning the check directly is +/// deliberate: caching the results in static fields (for example, via a static constructor) would +/// defeat both the JIT folding and the trimmer — the trimmer cannot analyze a static constructor, +/// so it would be unable to prove which branches are dead and would keep OS-specific code (including /// Windows-only native dependencies) in apps published for other platforms. /// /// @@ -67,17 +70,29 @@ internal static class OsConstants /// /// Gets a value indicating whether the runtime is executing on Windows. /// + #if NET + internal static bool IsWindows => OperatingSystem.IsWindows(); + #else internal static bool IsWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + #endif /// /// Gets a value indicating whether the runtime is executing on Linux. /// + #if NET + internal static bool IsLinux => OperatingSystem.IsLinux(); + #else internal static bool IsLinux => RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + #endif /// /// Gets a value indicating whether the runtime is executing on macOS. /// + #if NET + internal static bool IsMacOS => OperatingSystem.IsMacOS(); + #else internal static bool IsMacOS => RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + #endif #if NET /// @@ -87,6 +102,6 @@ internal static class OsConstants /// FreeBSD support is only available in .NET 5+ and later. This property will be /// false on .NET Framework or if the runtime does not support FreeBSD detection. /// - internal static bool IsFreeBSD => RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD); + internal static bool IsFreeBSD => OperatingSystem.IsFreeBSD(); #endif }