From b39ddf91ad308b026224a92a686a31c841551894 Mon Sep 17 00:00:00 2001 From: Philo Date: Mon, 21 Jul 2025 15:44:29 -0700 Subject: [PATCH] Make ConfigurationOptions IDisposable --- docs/ReleaseNotes.md | 2 +- .../ConfigurationOptions.cs | 158 ++++++++++++------ .../PublicAPI/PublicAPI.Shipped.txt | 1 + .../StackExchange.Redis.Tests/ConfigTests.cs | 17 ++ 4 files changed, 124 insertions(+), 54 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 758e0a30f..9ee5e42fd 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,7 +8,7 @@ Current package versions: ## Unreleased -- nothing yet +- Make ConfigurationOptions IDisposable ([#2922 by philon-msft](https://github.com/StackExchange/StackExchange.Redis/pull/2922)) ## 2.8.58 diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index dfdab5f4e..e917cefa3 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -28,7 +28,7 @@ namespace StackExchange.Redis /// /// /// - public sealed class ConfigurationOptions : ICloneable + public sealed class ConfigurationOptions : ICloneable, IDisposable { private static class OptionKeys { @@ -178,6 +178,8 @@ public static string TryNormalize(string value) private ILoggerFactory? loggerFactory; + private bool _isDisposed = false; + /// /// A LocalCertificateSelectionCallback delegate responsible for selecting the certificate used for authentication; note /// that this cannot be specified in the configuration-string. @@ -197,8 +199,16 @@ public static string TryNormalize(string value) /// public DefaultOptionsProvider Defaults { - get => defaultOptions ??= DefaultOptionsProvider.GetProvider(EndPoints); - set => defaultOptions = value; + get + { + ThrowIfDisposed(); + return defaultOptions ??= DefaultOptionsProvider.GetProvider(EndPoints); + } + set + { + ThrowIfDisposed(); + defaultOptions = value; + } } /// @@ -305,8 +315,8 @@ public bool HighIntegrity /// /// Supply a user certificate from a PEM file pair and enable TLS. /// - /// The path for the the user certificate (commonly a .crt file). - /// The path for the the user key (commonly a .key file). + /// The path for the user certificate (commonly a .crt file). + /// The path for the user key (commonly a .key file). public void SetUserPemCertificate(string userCertificatePath, string? userKeyPath = null) { CertificateSelectionCallback = CreatePemUserCertificateCallback(userCertificatePath, userKeyPath); @@ -317,7 +327,7 @@ public void SetUserPemCertificate(string userCertificatePath, string? userKeyPat /// /// Supply a user certificate from a PFX file and optional password and enable TLS. /// - /// The path for the the user certificate (commonly a .pfx file). + /// The path for the user certificate (commonly a .pfx file). /// The password for the certificate file. public void SetUserPfxCertificate(string userCertificatePath, string? password = null) { @@ -383,14 +393,14 @@ private static bool CheckTrustedIssuer(X509Certificate2 certificateToValidate, X chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; chain.ChainPolicy.VerificationTime = chainToValidate?.ChainPolicy?.VerificationTime ?? DateTime.Now; chain.ChainPolicy.UrlRetrievalTimeout = new TimeSpan(0, 0, 0); - // Ensure entended key usage checks are run and that we're observing a server TLS certificate + // Ensure intended key usage checks are run and that we're observing a server TLS certificate chain.ChainPolicy.ApplicationPolicy.Add(_serverAuthOid); chain.ChainPolicy.ExtraStore.Add(authority); try { // This only verifies that the chain is valid, but with AllowUnknownCertificateAuthority could trust - // self-signed or partial chained vertificates + // self-signed or partial chained certificates var chainIsVerified = chain.Build(certificateToValidate); if (chainIsVerified) { @@ -787,53 +797,58 @@ public static ConfigurationOptions Parse(string configuration, bool ignoreUnknow /// /// Create a copy of the configuration. /// - public ConfigurationOptions Clone() => new ConfigurationOptions + public ConfigurationOptions Clone() { - defaultOptions = defaultOptions, - ClientName = ClientName, - ServiceName = ServiceName, - keepAlive = keepAlive, - syncTimeout = syncTimeout, - asyncTimeout = asyncTimeout, - allowAdmin = allowAdmin, - defaultVersion = defaultVersion, - connectTimeout = connectTimeout, - user = user, - password = password, - tieBreaker = tieBreaker, - ssl = ssl, - sslHost = sslHost, - configChannel = configChannel, - abortOnConnectFail = abortOnConnectFail, - resolveDns = resolveDns, - proxy = proxy, - commandMap = commandMap, - CertificateValidationCallback = CertificateValidationCallback, - CertificateSelectionCallback = CertificateSelectionCallback, - ChannelPrefix = ChannelPrefix.Clone(), - SocketManager = SocketManager, - connectRetry = connectRetry, - configCheckSeconds = configCheckSeconds, - responseTimeout = responseTimeout, - DefaultDatabase = DefaultDatabase, - reconnectRetryPolicy = reconnectRetryPolicy, - backlogPolicy = backlogPolicy, - SslProtocols = SslProtocols, - checkCertificateRevocation = checkCertificateRevocation, - BeforeSocketConnect = BeforeSocketConnect, - EndPoints = EndPoints.Clone(), - LoggerFactory = LoggerFactory, + ThrowIfDisposed(); + + return new ConfigurationOptions + { + defaultOptions = defaultOptions, + ClientName = ClientName, + ServiceName = ServiceName, + keepAlive = keepAlive, + syncTimeout = syncTimeout, + asyncTimeout = asyncTimeout, + allowAdmin = allowAdmin, + defaultVersion = defaultVersion, + connectTimeout = connectTimeout, + user = user, + password = password, + tieBreaker = tieBreaker, + ssl = ssl, + sslHost = sslHost, + configChannel = configChannel, + abortOnConnectFail = abortOnConnectFail, + resolveDns = resolveDns, + proxy = proxy, + commandMap = commandMap, + CertificateValidationCallback = CertificateValidationCallback, + CertificateSelectionCallback = CertificateSelectionCallback, + ChannelPrefix = ChannelPrefix.Clone(), + SocketManager = SocketManager, + connectRetry = connectRetry, + configCheckSeconds = configCheckSeconds, + responseTimeout = responseTimeout, + DefaultDatabase = DefaultDatabase, + reconnectRetryPolicy = reconnectRetryPolicy, + backlogPolicy = backlogPolicy, + SslProtocols = SslProtocols, + checkCertificateRevocation = checkCertificateRevocation, + BeforeSocketConnect = BeforeSocketConnect, + EndPoints = EndPoints.Clone(), + LoggerFactory = LoggerFactory, #if NETCOREAPP3_1_OR_GREATER - SslClientAuthenticationOptions = SslClientAuthenticationOptions, + SslClientAuthenticationOptions = SslClientAuthenticationOptions, #endif - Tunnel = Tunnel, - setClientLibrary = setClientLibrary, - LibraryName = LibraryName, - Protocol = Protocol, - heartbeatInterval = heartbeatInterval, - heartbeatConsistencyChecks = heartbeatConsistencyChecks, - highIntegrity = highIntegrity, - }; + Tunnel = Tunnel, + setClientLibrary = setClientLibrary, + LibraryName = LibraryName, + Protocol = Protocol, + heartbeatInterval = heartbeatInterval, + heartbeatConsistencyChecks = heartbeatConsistencyChecks, + highIntegrity = highIntegrity, + }; + } /// /// Apply settings to configure this instance of , e.g. for a specific scenario. @@ -922,7 +937,8 @@ public string ToString(bool includePassword) commandMap?.AppendDeltas(sb); return sb.ToString(); - static string? FormatProtocol(RedisProtocol? protocol) => protocol switch { + static string? FormatProtocol(RedisProtocol? protocol) => protocol switch + { null => null, RedisProtocol.Resp2 => "resp2", RedisProtocol.Resp3 => "resp3", @@ -1204,5 +1220,41 @@ internal static bool TryParseRedisProtocol(string? value, out RedisProtocol prot protocol = default; return false; } + + /// + /// Release all resources associated with this configuration. + /// + public void Dispose() + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + GC.SuppressFinalize(this); + + BeforeSocketConnect = null; + CertificateSelection = null; + CertificateValidation = null; + + backlogPolicy = null; + commandMap = null; + loggerFactory = null; + reconnectRetryPolicy = null; +#if NETCOREAPP3_1_OR_GREATER + SslClientAuthenticationOptions = null; +#endif + + try { (defaultOptions as IDisposable)?.Dispose(); } catch { } + } + + private void ThrowIfDisposed() + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(ConfigurationOptions)); + } + } } } diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 43b35ba58..cc5facbb4 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -246,6 +246,7 @@ StackExchange.Redis.ConfigurationOptions.Defaults.get -> StackExchange.Redis.Con StackExchange.Redis.ConfigurationOptions.Defaults.set -> void StackExchange.Redis.ConfigurationOptions.DefaultVersion.get -> System.Version! StackExchange.Redis.ConfigurationOptions.DefaultVersion.set -> void +StackExchange.Redis.ConfigurationOptions.Dispose() -> void StackExchange.Redis.ConfigurationOptions.EndPoints.get -> StackExchange.Redis.EndPointCollection! StackExchange.Redis.ConfigurationOptions.EndPoints.init -> void StackExchange.Redis.ConfigurationOptions.HeartbeatConsistencyChecks.get -> bool diff --git a/tests/StackExchange.Redis.Tests/ConfigTests.cs b/tests/StackExchange.Redis.Tests/ConfigTests.cs index 995b66a5a..5d8d326eb 100644 --- a/tests/StackExchange.Redis.Tests/ConfigTests.cs +++ b/tests/StackExchange.Redis.Tests/ConfigTests.cs @@ -34,6 +34,7 @@ public void ExpectedFields() Assert.Equal( new[] { + "_isDisposed", "abortOnConnectFail", "allowAdmin", "asyncTimeout", @@ -761,4 +762,20 @@ public void CheckHighIntegrity(bool? assigned, bool expected, string cs) var parsed = ConfigurationOptions.Parse(cs); Assert.Equal(expected, parsed.HighIntegrity); } + + [Fact] + public void DisposedThrows() + { + var options = ConfigurationOptions.Parse("myhost"); + Assert.IsType(options.Defaults); + + options.Dispose(); + + Assert.Throws(() => options.Defaults); + Assert.Throws(() => options.BacklogPolicy); + Assert.Throws(() => options.CommandMap); + Assert.Throws(() => options.LoggerFactory); + Assert.Throws(() => options.ReconnectRetryPolicy); + Assert.Throws(() => options.Clone()); + } }