From e68eb7e9fa39af55b3f90d88589b0d6d17036c58 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Thu, 20 Nov 2025 19:52:57 +0000 Subject: [PATCH 1/5] Reuse XmlWriterSettings. Lazily initialize these where the class might be used without needing to support XML. --- .../Data/SqlClient/Server/ValueUtilsSmi.cs | 8 +++++--- .../src/Microsoft/Data/SqlClient/SqlStream.cs | 7 +++---- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 13 ++++++------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Server/ValueUtilsSmi.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Server/ValueUtilsSmi.cs index 878efe58bc..6a78d8bf6d 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Server/ValueUtilsSmi.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Server/ValueUtilsSmi.cs @@ -35,7 +35,9 @@ internal static class ValueUtilsSmi // Constants private const int DefaultBinaryBufferSize = 4096; // Size of the buffer used to read input parameter of type Stream - private const int DefaultTextBufferSize = 4096; // Size of the buffer (in chars) user to read input parameter of type TextReader + private const int DefaultTextBufferSize = 4096; // Size of the buffer (in chars) user to read input parameter of type TextReader + + private static XmlWriterSettings s_writerSettings; // // User-visible semantics-laden Getter/Setter support methods @@ -3457,7 +3459,7 @@ private static void SetSqlXml_Unchecked(ITypedSettersV3 setters, int ordinal, Sq private static void SetXmlReader_Unchecked(ITypedSettersV3 setters, int ordinal, XmlReader xmlReader) { // set up writer - XmlWriterSettings WriterSettings = new XmlWriterSettings + s_writerSettings ??= new XmlWriterSettings { CloseOutput = false, // don't close the memory stream ConformanceLevel = ConformanceLevel.Fragment, @@ -3466,7 +3468,7 @@ private static void SetXmlReader_Unchecked(ITypedSettersV3 setters, int ordinal, }; using (Stream target = new SmiSettersStream(setters, ordinal, SmiMetaData.DefaultXml)) - using (XmlWriter xmlWriter = XmlWriter.Create(target, WriterSettings)) + using (XmlWriter xmlWriter = XmlWriter.Create(target, s_writerSettings)) { // now spool the data into the writer (WriteNode will call Read()) xmlReader.Read(); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlStream.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlStream.cs index ca0e90bab8..653faf7213 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlStream.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlStream.cs @@ -477,6 +477,8 @@ private long TotalLength sealed internal class SqlStreamingXml { + private static readonly XmlWriterSettings s_writerSettings = new() { CloseOutput = true, ConformanceLevel = ConformanceLevel.Fragment }; + private readonly int _columnOrdinal; private SqlDataReader _reader; private XmlReader _xmlReader; @@ -509,10 +511,7 @@ public long GetChars(long dataIndex, char[] buffer, int bufferIndex, int length) SqlStream sqlStream = new(_columnOrdinal, _reader, addByteOrderMark: true, processAllRows:false, advanceReader:false); _xmlReader = sqlStream.ToXmlReader(); _strWriter = new StringWriter((System.IFormatProvider)null); - XmlWriterSettings writerSettings = new(); - writerSettings.CloseOutput = true; // close the memory stream when done - writerSettings.ConformanceLevel = ConformanceLevel.Fragment; - _xmlWriter = XmlWriter.Create(_strWriter, writerSettings); + _xmlWriter = XmlWriter.Create(_strWriter, s_writerSettings); } int charsToSkip = 0; 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 115c62f6c5..ba7689b907 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -41,6 +41,9 @@ namespace Microsoft.Data.SqlClient internal sealed partial class TdsParser { private static int _objectTypeCount; // EventSource counter + private static XmlWriterSettings s_asyncWriterSettings; + private static XmlWriterSettings s_syncWriterSettings; + private readonly SqlClientLogger _logger = new SqlClientLogger(); private SspiContextProvider _authenticationProvider; @@ -12768,13 +12771,9 @@ private async Task WriteXmlFeed(XmlDataFeed feed, TdsParserStateObject stateObj, preambleToSkip = encoding.GetPreamble(); } - XmlWriterSettings writerSettings = new XmlWriterSettings(); - writerSettings.CloseOutput = false; // don't close the memory stream - writerSettings.ConformanceLevel = ConformanceLevel.Fragment; - if (_asyncWrite) - { - writerSettings.Async = true; - } + XmlWriterSettings writerSettings = _asyncWrite + ? (s_asyncWriterSettings ??= new() { CloseOutput = false, ConformanceLevel = ConformanceLevel.Fragment, Async = true }) + : (s_syncWriterSettings ??= new() { CloseOutput = false, ConformanceLevel = ConformanceLevel.Fragment }); using ConstrainedTextWriter writer = new ConstrainedTextWriter(new StreamWriter(new TdsOutputStream(this, stateObj, preambleToSkip), encoding), size); using XmlWriter ww = XmlWriter.Create(writer, writerSettings); From aa48140268701278e601616bb52cab5a64ad3201 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Thu, 20 Nov 2025 20:37:09 +0000 Subject: [PATCH 2/5] Eliminate allocation of MemoryCacheEntryOptions --- .../SqlClient/AzureAttestationBasedEnclaveProvider.cs | 6 +----- .../Microsoft/Data/SqlClient/EnclaveProviderBase.cs | 7 +------ .../Microsoft/Data/SqlClient/EnclaveSessionCache.cs | 10 ++-------- .../Data/SqlClient/SignatureVerificationCache.cs | 6 +----- .../Microsoft/Data/SqlClient/SqlQueryMetadataCache.cs | 10 ++++------ .../Microsoft/Data/SqlClient/SqlSymmetricKeyCache.cs | 6 +----- .../SqlClient/VirtualSecureModeEnclaveProviderBase.cs | 6 +----- 7 files changed, 11 insertions(+), 40 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AzureAttestationBasedEnclaveProvider.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AzureAttestationBasedEnclaveProvider.cs index 70c6de60aa..a89ca42aa5 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AzureAttestationBasedEnclaveProvider.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AzureAttestationBasedEnclaveProvider.cs @@ -349,11 +349,7 @@ private OpenIdConnectConfiguration GetOpenIdConfigForSigningKeys(string url, boo throw SQL.AttestationFailed(string.Format(Strings.GetAttestationTokenSigningKeysFailed, GetInnerMostExceptionMessage(exception)), exception); } - MemoryCacheEntryOptions options = new MemoryCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(1) - }; - OpenIdConnectConfigurationCache.Set(url, openIdConnectConfig, options); + OpenIdConnectConfigurationCache.Set(url, openIdConnectConfig, absoluteExpirationRelativeToNow: TimeSpan.FromDays(1)); } return openIdConnectConfig; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveProviderBase.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveProviderBase.cs index b666819dde..f49e847801 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveProviderBase.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveProviderBase.cs @@ -68,7 +68,6 @@ internal abstract class EnclaveProviderBase : SqlColumnEncryptionEnclaveProvider { #region Constants private const int NonceSize = 256; - private const int ThreadRetryCacheTimeoutInMinutes = 10; private const int LockTimeoutMaxInMilliseconds = 15 * 1000; // 15 seconds #endregion @@ -167,11 +166,7 @@ protected void GetEnclaveSessionHelper(EnclaveSessionParameters enclaveSessionPa retryThreadID = Thread.CurrentThread.ManagedThreadId.ToString(); } - MemoryCacheEntryOptions options = new MemoryCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(ThreadRetryCacheTimeoutInMinutes) - }; - ThreadRetryCache.Set(Thread.CurrentThread.ManagedThreadId.ToString(), retryThreadID, options); + ThreadRetryCache.Set(Thread.CurrentThread.ManagedThreadId.ToString(), retryThreadID, absoluteExpirationRelativeToNow: TimeSpan.FromMinutes(10)); } } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveSessionCache.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveSessionCache.cs index 5673395e11..305f783324 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveSessionCache.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveSessionCache.cs @@ -18,9 +18,6 @@ internal class EnclaveSessionCache // given that for Always Encrypted scenarios, the server is considered an "untrusted" man-in-the-middle. private long _counter; - // Cache timeout of 8 hours to be consistent with jwt validity. - private static int enclaveCacheTimeOutInHours = 8; - // Retrieves a SqlEnclaveSession from the cache internal SqlEnclaveSession GetEnclaveSession(EnclaveSessionParameters enclaveSessionParameters, out long counter) { @@ -62,11 +59,8 @@ internal SqlEnclaveSession CreateSession(EnclaveSessionParameters enclaveSession lock (enclaveCacheLock) { enclaveSession = new SqlEnclaveSession(sharedSecret, sessionId); - MemoryCacheEntryOptions options = new MemoryCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(enclaveCacheTimeOutInHours) - }; - enclaveMemoryCache.Set(cacheKey, enclaveSession, options); + // Cache timeout of 8 hours to be consistent with JWT validity. + enclaveMemoryCache.Set(cacheKey, enclaveSession, absoluteExpirationRelativeToNow: TimeSpan.FromHours(8)); counter = Interlocked.Increment(ref _counter); } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SignatureVerificationCache.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SignatureVerificationCache.cs index 40d4ed9b79..c5d049adba 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SignatureVerificationCache.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SignatureVerificationCache.cs @@ -76,11 +76,7 @@ internal void AddSignatureVerificationResult(string keyStoreName, string masterK TrimCacheIfNeeded(); // By default evict after 10 days. - MemoryCacheEntryOptions options = new MemoryCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(10) - }; - _cache.Set(cacheLookupKey, result, options); + _cache.Set(cacheLookupKey, result, absoluteExpirationRelativeToNow: TimeSpan.FromDays(10)); } private void ValidateSignatureNotNullOrEmpty(byte[] signature, string methodName) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlQueryMetadataCache.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlQueryMetadataCache.cs index 0af5ea44e6..7284508cca 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlQueryMetadataCache.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlQueryMetadataCache.cs @@ -236,15 +236,13 @@ internal void AddQueryMetadata(SqlCommand sqlCommand, bool ignoreQueriesWithRetu } // By default evict after 10 hours. - MemoryCacheEntryOptions options = new MemoryCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(10) - }; - _cache.Set>(cacheLookupKey, cipherMetadataDictionary, options); + TimeSpan expirationPeriod = TimeSpan.FromHours(10); + + _cache.Set>(cacheLookupKey, cipherMetadataDictionary, absoluteExpirationRelativeToNow: expirationPeriod); if (sqlCommand.requiresEnclaveComputations) { ConcurrentDictionary keysToBeCached = CreateCopyOfEnclaveKeys(sqlCommand.keysToBeSentToEnclave); - _cache.Set>(enclaveLookupKey, keysToBeCached, options); + _cache.Set>(enclaveLookupKey, keysToBeCached, absoluteExpirationRelativeToNow: expirationPeriod); } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSymmetricKeyCache.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSymmetricKeyCache.cs index 961746fc2d..d88833abed 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSymmetricKeyCache.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSymmetricKeyCache.cs @@ -98,11 +98,7 @@ internal SqlClientSymmetricKey GetKey(SqlEncryptionKeyInfo keyInfo, SqlConnectio { // In case multiple threads reach here at the same time, the first one wins. // The allocated memory will be reclaimed by Garbage Collector. - MemoryCacheEntryOptions options = new() - { - AbsoluteExpirationRelativeToNow = SqlConnection.ColumnEncryptionKeyCacheTtl - }; - _cache.Set(cacheLookupKey, encryptionKey, options); + _cache.Set(cacheLookupKey, encryptionKey, absoluteExpirationRelativeToNow: SqlConnection.ColumnEncryptionKeyCacheTtl); } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/VirtualSecureModeEnclaveProviderBase.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/VirtualSecureModeEnclaveProviderBase.cs index 198ac9f10f..31ef1f69bf 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/VirtualSecureModeEnclaveProviderBase.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/VirtualSecureModeEnclaveProviderBase.cs @@ -211,11 +211,7 @@ private X509Certificate2Collection GetSigningCertificate(string attestationUrl, throw SQL.AttestationFailed(string.Format(Strings.GetAttestationSigningCertificateFailedInvalidCertificate, attestationUrl), exception); } - MemoryCacheEntryOptions options = new MemoryCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(1) - }; - rootSigningCertificateCache.Set(attestationUrl, certificateCollection, options); + rootSigningCertificateCache.Set(attestationUrl, certificateCollection, absoluteExpirationRelativeToNow: TimeSpan.FromDays(1)); } return rootSigningCertificateCache.Get(attestationUrl); From cbadbe78c332423e0b862364cd107ad6e7320818 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Fri, 21 Nov 2025 20:05:05 +0000 Subject: [PATCH 3/5] Response to code review Introduce and reintroduce constants containing cache expiration periods --- .../src/Microsoft/Data/SqlClient/EnclaveProviderBase.cs | 4 +++- .../src/Microsoft/Data/SqlClient/EnclaveSessionCache.cs | 7 +++++-- .../Microsoft/Data/SqlClient/SignatureVerificationCache.cs | 3 ++- .../src/Microsoft/Data/SqlClient/SqlQueryMetadataCache.cs | 4 ++-- .../Data/SqlClient/VirtualSecureModeEnclaveProviderBase.cs | 4 +++- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveProviderBase.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveProviderBase.cs index f49e847801..823948beea 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveProviderBase.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveProviderBase.cs @@ -68,6 +68,7 @@ internal abstract class EnclaveProviderBase : SqlColumnEncryptionEnclaveProvider { #region Constants private const int NonceSize = 256; + private const int ThreadRetryCacheTimeoutInMinutes = 10; private const int LockTimeoutMaxInMilliseconds = 15 * 1000; // 15 seconds #endregion @@ -166,7 +167,8 @@ protected void GetEnclaveSessionHelper(EnclaveSessionParameters enclaveSessionPa retryThreadID = Thread.CurrentThread.ManagedThreadId.ToString(); } - ThreadRetryCache.Set(Thread.CurrentThread.ManagedThreadId.ToString(), retryThreadID, absoluteExpirationRelativeToNow: TimeSpan.FromMinutes(10)); + ThreadRetryCache.Set(Thread.CurrentThread.ManagedThreadId.ToString(), retryThreadID, + absoluteExpirationRelativeToNow: TimeSpan.FromMinutes(ThreadRetryCacheTimeoutInMinutes)); } } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveSessionCache.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveSessionCache.cs index 305f783324..68bb90d92c 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveSessionCache.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveSessionCache.cs @@ -11,6 +11,9 @@ namespace Microsoft.Data.SqlClient // Maintains a cache of SqlEnclaveSession instances internal class EnclaveSessionCache { + // Cache timeout of 8 hours to be consistent with JWT validity. + private const int EnclaveCacheTimeOutInHours = 8; + private readonly MemoryCache enclaveMemoryCache = new MemoryCache(new MemoryCacheOptions()); private readonly object enclaveCacheLock = new object(); @@ -59,8 +62,8 @@ internal SqlEnclaveSession CreateSession(EnclaveSessionParameters enclaveSession lock (enclaveCacheLock) { enclaveSession = new SqlEnclaveSession(sharedSecret, sessionId); - // Cache timeout of 8 hours to be consistent with JWT validity. - enclaveMemoryCache.Set(cacheKey, enclaveSession, absoluteExpirationRelativeToNow: TimeSpan.FromHours(8)); + enclaveMemoryCache.Set(cacheKey, enclaveSession, + absoluteExpirationRelativeToNow: TimeSpan.FromHours(EnclaveCacheTimeOutInHours)); counter = Interlocked.Increment(ref _counter); } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SignatureVerificationCache.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SignatureVerificationCache.cs index c5d049adba..1f1cef7b67 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SignatureVerificationCache.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SignatureVerificationCache.cs @@ -16,6 +16,7 @@ internal class ColumnMasterKeyMetadataSignatureVerificationCache { private const int _cacheSize = 2000; // Cache size in number of entries. private const int _cacheTrimThreshold = 300; // Threshold above the cache size when we start trimming. + private const int VerificationCacheTimeOutInDays = 10; private const string _className = "ColumnMasterKeyMetadataSignatureVerificationCache"; private const string _getSignatureVerificationResultMethodName = "GetSignatureVerificationResult"; @@ -76,7 +77,7 @@ internal void AddSignatureVerificationResult(string keyStoreName, string masterK TrimCacheIfNeeded(); // By default evict after 10 days. - _cache.Set(cacheLookupKey, result, absoluteExpirationRelativeToNow: TimeSpan.FromDays(10)); + _cache.Set(cacheLookupKey, result, absoluteExpirationRelativeToNow: TimeSpan.FromDays(VerificationCacheTimeOutInDays)); } private void ValidateSignatureNotNullOrEmpty(byte[] signature, string methodName) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlQueryMetadataCache.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlQueryMetadataCache.cs index 7284508cca..33d0018f0e 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlQueryMetadataCache.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlQueryMetadataCache.cs @@ -21,6 +21,7 @@ sealed internal class SqlQueryMetadataCache { const int CacheSize = 2000; // Cache size in number of entries. const int CacheTrimThreshold = 300; // Threshold above the cache size when we start trimming. + private const int MetadataCacheTimeOutInHours = 10; private readonly MemoryCache _cache; private static readonly SqlQueryMetadataCache s_singletonInstance = new(); @@ -235,8 +236,7 @@ internal void AddQueryMetadata(SqlCommand sqlCommand, bool ignoreQueriesWithRetu } } - // By default evict after 10 hours. - TimeSpan expirationPeriod = TimeSpan.FromHours(10); + TimeSpan expirationPeriod = TimeSpan.FromHours(MetadataCacheTimeOutInHours); _cache.Set>(cacheLookupKey, cipherMetadataDictionary, absoluteExpirationRelativeToNow: expirationPeriod); if (sqlCommand.requiresEnclaveComputations) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/VirtualSecureModeEnclaveProviderBase.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/VirtualSecureModeEnclaveProviderBase.cs index 31ef1f69bf..93d7e01cae 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/VirtualSecureModeEnclaveProviderBase.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/VirtualSecureModeEnclaveProviderBase.cs @@ -24,6 +24,7 @@ internal abstract class VirtualizationBasedSecurityEnclaveProviderBase : Enclave private const int DiffieHellmanKeySize = 384; private const int VsmHGSProtocolId = (int)SqlConnectionAttestationProtocol.HGS; + private const int RootSigningCertificateCacheTimeOutInDays = 1; // ENCLAVE_IDENTITY related constants private static readonly EnclaveIdentity ExpectedPolicy = new EnclaveIdentity() @@ -211,7 +212,8 @@ private X509Certificate2Collection GetSigningCertificate(string attestationUrl, throw SQL.AttestationFailed(string.Format(Strings.GetAttestationSigningCertificateFailedInvalidCertificate, attestationUrl), exception); } - rootSigningCertificateCache.Set(attestationUrl, certificateCollection, absoluteExpirationRelativeToNow: TimeSpan.FromDays(1)); + rootSigningCertificateCache.Set(attestationUrl, certificateCollection, + absoluteExpirationRelativeToNow: TimeSpan.FromDays(RootSigningCertificateCacheTimeOutInDays)); } return rootSigningCertificateCache.Get(attestationUrl); From 704fa8c2f9eb08bdad039560b7b983f6878ab641 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Fri, 21 Nov 2025 20:07:40 +0000 Subject: [PATCH 4/5] Brief cleanup of constant naming convention for consistency --- .../Data/SqlClient/SignatureVerificationCache.cs | 8 ++++---- .../src/Microsoft/Data/SqlClient/SqlQueryMetadataCache.cs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SignatureVerificationCache.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SignatureVerificationCache.cs index 1f1cef7b67..77bcd019d3 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SignatureVerificationCache.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SignatureVerificationCache.cs @@ -14,8 +14,8 @@ namespace Microsoft.Data.SqlClient /// internal class ColumnMasterKeyMetadataSignatureVerificationCache { - private const int _cacheSize = 2000; // Cache size in number of entries. - private const int _cacheTrimThreshold = 300; // Threshold above the cache size when we start trimming. + private const int CacheSize = 2000; // Cache size in number of entries. + private const int CacheTrimThreshold = 300; // Threshold above the cache size when we start trimming. private const int VerificationCacheTimeOutInDays = 10; private const string _className = "ColumnMasterKeyMetadataSignatureVerificationCache"; @@ -114,12 +114,12 @@ private void TrimCacheIfNeeded() { // If the size of the cache exceeds the threshold, set that we are in trimming and trim the cache accordingly. long currentCacheSize = _cache.Count; - if ((currentCacheSize > _cacheSize + _cacheTrimThreshold) && (0 == Interlocked.CompareExchange(ref _inTrim, 1, 0))) + if ((currentCacheSize > CacheSize + CacheTrimThreshold) && (0 == Interlocked.CompareExchange(ref _inTrim, 1, 0))) { try { // Example: 2301 - 2000 = 301; 301 / 2301 = 0.1308 * 100 = 13% compacting - _cache.Compact((((double)(currentCacheSize - _cacheSize) / (double)currentCacheSize) * 100)); + _cache.Compact((((double)(currentCacheSize - CacheSize) / (double)currentCacheSize) * 100)); } finally { diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlQueryMetadataCache.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlQueryMetadataCache.cs index 33d0018f0e..9a065b371a 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlQueryMetadataCache.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlQueryMetadataCache.cs @@ -19,8 +19,8 @@ namespace Microsoft.Data.SqlClient /// sealed internal class SqlQueryMetadataCache { - const int CacheSize = 2000; // Cache size in number of entries. - const int CacheTrimThreshold = 300; // Threshold above the cache size when we start trimming. + private const int CacheSize = 2000; // Cache size in number of entries. + private const int CacheTrimThreshold = 300; // Threshold above the cache size when we start trimming. private const int MetadataCacheTimeOutInHours = 10; private readonly MemoryCache _cache; From 843a56eac7f1f980ef243bddbd736fc63d94f1d9 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Tue, 25 Nov 2025 22:44:21 +0000 Subject: [PATCH 5/5] Respond to code review Turn the lazy field initialisation into a property, and the TimeOutInX constants which were only used to create a TimeSpan into a static readonly TimeSpan. --- .../AzureAttestationBasedEnclaveProvider.cs | 3 ++- .../Data/SqlClient/EnclaveProviderBase.cs | 4 ++-- .../Data/SqlClient/EnclaveSessionCache.cs | 4 ++-- .../Data/SqlClient/Server/ValueUtilsSmi.cs | 23 +++++++++++-------- .../SqlClient/SignatureVerificationCache.cs | 4 ++-- .../Data/SqlClient/SqlQueryMetadataCache.cs | 8 +++---- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 15 ++++++++---- .../VirtualSecureModeEnclaveProviderBase.cs | 4 ++-- 8 files changed, 36 insertions(+), 29 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AzureAttestationBasedEnclaveProvider.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AzureAttestationBasedEnclaveProvider.cs index a89ca42aa5..880ab081f0 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AzureAttestationBasedEnclaveProvider.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AzureAttestationBasedEnclaveProvider.cs @@ -60,6 +60,7 @@ internal class AzureAttestationEnclaveProvider : EnclaveProviderBase private const string AttestationUrlSuffix = @"/.well-known/openid-configuration"; private static readonly MemoryCache OpenIdConnectConfigurationCache = new MemoryCache(new MemoryCacheOptions()); + private static readonly TimeSpan s_openIdConnectConfigurationCacheTimeout = TimeSpan.FromDays(1); #endregion #region Internal methods @@ -349,7 +350,7 @@ private OpenIdConnectConfiguration GetOpenIdConfigForSigningKeys(string url, boo throw SQL.AttestationFailed(string.Format(Strings.GetAttestationTokenSigningKeysFailed, GetInnerMostExceptionMessage(exception)), exception); } - OpenIdConnectConfigurationCache.Set(url, openIdConnectConfig, absoluteExpirationRelativeToNow: TimeSpan.FromDays(1)); + OpenIdConnectConfigurationCache.Set(url, openIdConnectConfig, absoluteExpirationRelativeToNow: s_openIdConnectConfigurationCacheTimeout); } return openIdConnectConfig; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveProviderBase.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveProviderBase.cs index 823948beea..c81f04471c 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveProviderBase.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveProviderBase.cs @@ -68,7 +68,6 @@ internal abstract class EnclaveProviderBase : SqlColumnEncryptionEnclaveProvider { #region Constants private const int NonceSize = 256; - private const int ThreadRetryCacheTimeoutInMinutes = 10; private const int LockTimeoutMaxInMilliseconds = 15 * 1000; // 15 seconds #endregion @@ -85,6 +84,7 @@ internal abstract class EnclaveProviderBase : SqlColumnEncryptionEnclaveProvider // It is used to save the attestation url and nonce value across API calls protected static readonly MemoryCache ThreadRetryCache = new MemoryCache(new MemoryCacheOptions()); + private static readonly TimeSpan s_threadRetryCacheTimeout = TimeSpan.FromMinutes(10); #endregion #region protected methods @@ -168,7 +168,7 @@ protected void GetEnclaveSessionHelper(EnclaveSessionParameters enclaveSessionPa } ThreadRetryCache.Set(Thread.CurrentThread.ManagedThreadId.ToString(), retryThreadID, - absoluteExpirationRelativeToNow: TimeSpan.FromMinutes(ThreadRetryCacheTimeoutInMinutes)); + absoluteExpirationRelativeToNow: s_threadRetryCacheTimeout); } } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveSessionCache.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveSessionCache.cs index 68bb90d92c..c3845244f3 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveSessionCache.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveSessionCache.cs @@ -12,7 +12,7 @@ namespace Microsoft.Data.SqlClient internal class EnclaveSessionCache { // Cache timeout of 8 hours to be consistent with JWT validity. - private const int EnclaveCacheTimeOutInHours = 8; + private static readonly TimeSpan s_enclaveCacheTimeout = TimeSpan.FromHours(8); private readonly MemoryCache enclaveMemoryCache = new MemoryCache(new MemoryCacheOptions()); private readonly object enclaveCacheLock = new object(); @@ -63,7 +63,7 @@ internal SqlEnclaveSession CreateSession(EnclaveSessionParameters enclaveSession { enclaveSession = new SqlEnclaveSession(sharedSecret, sessionId); enclaveMemoryCache.Set(cacheKey, enclaveSession, - absoluteExpirationRelativeToNow: TimeSpan.FromHours(EnclaveCacheTimeOutInHours)); + absoluteExpirationRelativeToNow: s_enclaveCacheTimeout); counter = Interlocked.Increment(ref _counter); } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Server/ValueUtilsSmi.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Server/ValueUtilsSmi.cs index 6a78d8bf6d..0b75a6178d 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Server/ValueUtilsSmi.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Server/ValueUtilsSmi.cs @@ -37,7 +37,18 @@ internal static class ValueUtilsSmi private const int DefaultBinaryBufferSize = 4096; // Size of the buffer used to read input parameter of type Stream private const int DefaultTextBufferSize = 4096; // Size of the buffer (in chars) user to read input parameter of type TextReader - private static XmlWriterSettings s_writerSettings; + // @TODO: Replace field with the `field` keyword when LangVersion >= 14. + private static XmlWriterSettings s_xmlWriterSettings; + + private static XmlWriterSettings XmlWriterSettings => + s_xmlWriterSettings ??= new XmlWriterSettings() + { + // Don't close the memory stream + CloseOutput = false, + ConformanceLevel = ConformanceLevel.Fragment, + Encoding = System.Text.Encoding.Unicode, + OmitXmlDeclaration = true + }; // // User-visible semantics-laden Getter/Setter support methods @@ -3459,16 +3470,8 @@ private static void SetSqlXml_Unchecked(ITypedSettersV3 setters, int ordinal, Sq private static void SetXmlReader_Unchecked(ITypedSettersV3 setters, int ordinal, XmlReader xmlReader) { // set up writer - s_writerSettings ??= new XmlWriterSettings - { - CloseOutput = false, // don't close the memory stream - ConformanceLevel = ConformanceLevel.Fragment, - Encoding = System.Text.Encoding.Unicode, - OmitXmlDeclaration = true - }; - using (Stream target = new SmiSettersStream(setters, ordinal, SmiMetaData.DefaultXml)) - using (XmlWriter xmlWriter = XmlWriter.Create(target, s_writerSettings)) + using (XmlWriter xmlWriter = XmlWriter.Create(target, XmlWriterSettings)) { // now spool the data into the writer (WriteNode will call Read()) xmlReader.Read(); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SignatureVerificationCache.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SignatureVerificationCache.cs index 77bcd019d3..8d1ce98df1 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SignatureVerificationCache.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SignatureVerificationCache.cs @@ -16,7 +16,6 @@ internal class ColumnMasterKeyMetadataSignatureVerificationCache { private const int CacheSize = 2000; // Cache size in number of entries. private const int CacheTrimThreshold = 300; // Threshold above the cache size when we start trimming. - private const int VerificationCacheTimeOutInDays = 10; private const string _className = "ColumnMasterKeyMetadataSignatureVerificationCache"; private const string _getSignatureVerificationResultMethodName = "GetSignatureVerificationResult"; @@ -27,6 +26,7 @@ internal class ColumnMasterKeyMetadataSignatureVerificationCache private const string _cacheLookupKeySeparator = ":"; private static readonly ColumnMasterKeyMetadataSignatureVerificationCache _signatureVerificationCache = new ColumnMasterKeyMetadataSignatureVerificationCache(); + private static readonly TimeSpan s_verificationCacheTimeout = TimeSpan.FromDays(10); //singleton instance internal static ColumnMasterKeyMetadataSignatureVerificationCache Instance { get { return _signatureVerificationCache; } } @@ -77,7 +77,7 @@ internal void AddSignatureVerificationResult(string keyStoreName, string masterK TrimCacheIfNeeded(); // By default evict after 10 days. - _cache.Set(cacheLookupKey, result, absoluteExpirationRelativeToNow: TimeSpan.FromDays(VerificationCacheTimeOutInDays)); + _cache.Set(cacheLookupKey, result, absoluteExpirationRelativeToNow: s_verificationCacheTimeout); } private void ValidateSignatureNotNullOrEmpty(byte[] signature, string methodName) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlQueryMetadataCache.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlQueryMetadataCache.cs index 9a065b371a..a4a35840ac 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlQueryMetadataCache.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlQueryMetadataCache.cs @@ -21,10 +21,10 @@ sealed internal class SqlQueryMetadataCache { private const int CacheSize = 2000; // Cache size in number of entries. private const int CacheTrimThreshold = 300; // Threshold above the cache size when we start trimming. - private const int MetadataCacheTimeOutInHours = 10; private readonly MemoryCache _cache; private static readonly SqlQueryMetadataCache s_singletonInstance = new(); + private static readonly TimeSpan s_metadataCacheTimeout = TimeSpan.FromHours(10); private int _inTrim = 0; private long _cacheHits = 0; private long _cacheMisses = 0; @@ -236,13 +236,11 @@ internal void AddQueryMetadata(SqlCommand sqlCommand, bool ignoreQueriesWithRetu } } - TimeSpan expirationPeriod = TimeSpan.FromHours(MetadataCacheTimeOutInHours); - - _cache.Set>(cacheLookupKey, cipherMetadataDictionary, absoluteExpirationRelativeToNow: expirationPeriod); + _cache.Set>(cacheLookupKey, cipherMetadataDictionary, absoluteExpirationRelativeToNow: s_metadataCacheTimeout); if (sqlCommand.requiresEnclaveComputations) { ConcurrentDictionary keysToBeCached = CreateCopyOfEnclaveKeys(sqlCommand.keysToBeSentToEnclave); - _cache.Set>(enclaveLookupKey, keysToBeCached, absoluteExpirationRelativeToNow: expirationPeriod); + _cache.Set>(enclaveLookupKey, keysToBeCached, absoluteExpirationRelativeToNow: s_metadataCacheTimeout); } } 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 ba7689b907..35610c7844 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -41,8 +41,9 @@ namespace Microsoft.Data.SqlClient internal sealed partial class TdsParser { private static int _objectTypeCount; // EventSource counter - private static XmlWriterSettings s_asyncWriterSettings; - private static XmlWriterSettings s_syncWriterSettings; + // @TODO: Replace both fields with the `field` keyword when LangVersion >= 14. + private static XmlWriterSettings s_asyncXmlWriterSettings; + private static XmlWriterSettings s_syncXmlWriterSettings; private readonly SqlClientLogger _logger = new SqlClientLogger(); @@ -157,6 +158,12 @@ internal sealed partial class TdsParser // NOTE: You must take the internal connection's _parserLock before modifying this internal bool _asyncWrite = false; + private static XmlWriterSettings AsyncXmlWriterSettings => + s_asyncXmlWriterSettings ??= new() { CloseOutput = false, ConformanceLevel = ConformanceLevel.Fragment, Async = true }; + + private static XmlWriterSettings SyncXmlWriterSettings => + s_syncXmlWriterSettings ??= new() { CloseOutput = false, ConformanceLevel = ConformanceLevel.Fragment }; + /// /// Get or set if column encryption is supported by the server. /// @@ -12771,9 +12778,7 @@ private async Task WriteXmlFeed(XmlDataFeed feed, TdsParserStateObject stateObj, preambleToSkip = encoding.GetPreamble(); } - XmlWriterSettings writerSettings = _asyncWrite - ? (s_asyncWriterSettings ??= new() { CloseOutput = false, ConformanceLevel = ConformanceLevel.Fragment, Async = true }) - : (s_syncWriterSettings ??= new() { CloseOutput = false, ConformanceLevel = ConformanceLevel.Fragment }); + XmlWriterSettings writerSettings = _asyncWrite ? AsyncXmlWriterSettings : SyncXmlWriterSettings; using ConstrainedTextWriter writer = new ConstrainedTextWriter(new StreamWriter(new TdsOutputStream(this, stateObj, preambleToSkip), encoding), size); using XmlWriter ww = XmlWriter.Create(writer, writerSettings); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/VirtualSecureModeEnclaveProviderBase.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/VirtualSecureModeEnclaveProviderBase.cs index 93d7e01cae..a3b4545d03 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/VirtualSecureModeEnclaveProviderBase.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/VirtualSecureModeEnclaveProviderBase.cs @@ -17,6 +17,7 @@ internal abstract class VirtualizationBasedSecurityEnclaveProviderBase : Enclave #region Members private static readonly MemoryCache rootSigningCertificateCache = new MemoryCache(new MemoryCacheOptions()); + private static readonly TimeSpan s_rootSigningCertificateCacheTimeout = TimeSpan.FromDays(1); #endregion @@ -24,7 +25,6 @@ internal abstract class VirtualizationBasedSecurityEnclaveProviderBase : Enclave private const int DiffieHellmanKeySize = 384; private const int VsmHGSProtocolId = (int)SqlConnectionAttestationProtocol.HGS; - private const int RootSigningCertificateCacheTimeOutInDays = 1; // ENCLAVE_IDENTITY related constants private static readonly EnclaveIdentity ExpectedPolicy = new EnclaveIdentity() @@ -213,7 +213,7 @@ private X509Certificate2Collection GetSigningCertificate(string attestationUrl, } rootSigningCertificateCache.Set(attestationUrl, certificateCollection, - absoluteExpirationRelativeToNow: TimeSpan.FromDays(RootSigningCertificateCacheTimeOutInDays)); + absoluteExpirationRelativeToNow: s_rootSigningCertificateCacheTimeout); } return rootSigningCertificateCache.Get(attestationUrl);