diff --git a/src/Microsoft.Data.SqlClient.slnx b/src/Microsoft.Data.SqlClient.slnx index 9c03addd51..54863d7191 100644 --- a/src/Microsoft.Data.SqlClient.slnx +++ b/src/Microsoft.Data.SqlClient.slnx @@ -175,6 +175,10 @@ + + + + diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Reliability/SqlConfigurableRetryLogicLoader.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Reliability/SqlConfigurableRetryLogicLoader.cs index df556a49ba..a9340cd1b3 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Reliability/SqlConfigurableRetryLogicLoader.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Reliability/SqlConfigurableRetryLogicLoader.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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. @@ -357,28 +357,30 @@ internal sealed class AssemblyResolutionSubscription : IDisposable { #if NET private bool _isSubscribed; + private readonly AssemblyLoadContext _currentAssemblyLoadContext; #endif internal AssemblyResolutionSubscription(bool subscribe) { - #if NET + #if NET if (subscribe) { - AssemblyLoadContext.Default.Resolving += Default_Resolving; + _currentAssemblyLoadContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()); + _currentAssemblyLoadContext?.Resolving += Default_Resolving; _isSubscribed = true; } - #endif + #endif } public void Dispose() { - #if NET + #if NET if (_isSubscribed) { - AssemblyLoadContext.Default.Resolving -= Default_Resolving; + _currentAssemblyLoadContext?.Resolving -= Default_Resolving; _isSubscribed = false; } - #endif + #endif } } @@ -399,10 +401,11 @@ private static Assembly AssemblyResolver(AssemblyName arg) string methodName = nameof(AssemblyResolver); string fullPath = MakeFullPath(ProbingDirectory, arg.Name); + AssemblyLoadContext currentAlc = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()); SqlClientEventSource.Log.TryTraceEvent(" Looking for '{2}' assembly by '{3}' full path." , TypeName, methodName, arg, fullPath); - return fullPath == null ? null : AssemblyLoadContext.Default.LoadFromAssemblyPath(fullPath); + return fullPath != null && currentAlc != null ? currentAlc.LoadFromAssemblyPath(fullPath) : null; } /// @@ -411,8 +414,8 @@ private static Assembly AssemblyResolver(AssemblyName arg) /// /// This handler is only subscribed while a configured retry logic provider is being /// resolved and constructed, and only when a custom retry logic type has been configured. - /// It must not remain subscribed to after that, - /// because doing so changes assembly resolution behavior for the entire application. + /// It must not remain subscribed to the calling after that, + /// because doing so can change assembly resolution behavior for the entire application. /// private static Assembly Default_Resolving(AssemblyLoadContext arg1, AssemblyName arg2) { @@ -426,8 +429,8 @@ private static Assembly Default_Resolving(AssemblyLoadContext arg1, AssemblyName } /// - /// Performs a case-sensitive search to resolve the specified type name - /// and its related assemblies in default assembly load context if they aren't loaded yet. + /// Performs a case-sensitive search to resolve the specified type name and its related + /// assemblies in the calling assembly load context if they aren't loaded yet. /// /// Resolved type if it could resolve the type; otherwise, the `SqlConfigurableRetryFactory` type. private static Type LoadType(string fullyQualifiedName) diff --git a/src/Microsoft.Data.SqlClient/tests/Common/ConfigurableRetryLogic/RetryLogicConfigs.cs b/src/Microsoft.Data.SqlClient/tests/Common/ConfigurableRetryLogic/RetryLogicConfigs.cs new file mode 100644 index 0000000000..650a6d2acc --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/Common/ConfigurableRetryLogic/RetryLogicConfigs.cs @@ -0,0 +1,53 @@ +// 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. + +namespace Microsoft.Data.SqlClient.Tests.Common.ConfigurableRetryLogic; + +/// +/// Transport object for retry logic configuration parameters. This is used to pass +/// values which would ordinarily be part of the SqlConfigurableRetryLogicConnection +/// or SqlConfigurableRetryLogicCommand section to +/// +public sealed class RetryLogicConfigs +{ + /// + /// Equivalent of the deltaTime value in an app.config file. + /// + public TimeSpan DeltaTime { get; set; } + + /// + /// Equivalent of the maxTime value in an app.config file. + /// + public TimeSpan MaxTimeInterval { get; set; } + + /// + /// Equivalent of the minTime value in an app.config file. + /// + public TimeSpan MinTimeInterval { get; set; } + + /// + /// Equivalent of the numberOfTries value in an app.config file. + /// + public int NumberOfTries { get; set; } + + /// + /// Equivalent of the transientErrors value in an app.config file. + /// + public string? TransientErrors { get; set; } + + /// + /// Equivalent of the authorizedSqlCondition value in an app.config file. + /// + public string? AuthorizedSqlCondition { get; set; } + + /// + /// Equivalent of the retryLogicType value in an app.config file. + /// + public string? RetryLogicType { get; set; } + + /// + /// Equivalent of the retryMethod value in an app.config file. + /// + public string? RetryMethodName { get; set; } +} diff --git a/src/Microsoft.Data.SqlClient/tests/Common/ConfigurableRetryLogic/RetryLogicLoaderHelper.cs b/src/Microsoft.Data.SqlClient/tests/Common/ConfigurableRetryLogic/RetryLogicLoaderHelper.cs new file mode 100644 index 0000000000..b439aa3385 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/Common/ConfigurableRetryLogic/RetryLogicLoaderHelper.cs @@ -0,0 +1,128 @@ +// 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.Reflection; + +namespace Microsoft.Data.SqlClient.Tests.Common.ConfigurableRetryLogic; + +/// +/// Contains helper methods for creating and configuring instances of +/// SqlConfigurableRetryLogicLoader and its associated configuration objects without needing +/// to populate an app.config file. +/// +public static class RetryLogicLoaderHelper +{ + private const string ConfigurationLoaderTypeName = "Microsoft.Data.SqlClient.SqlConfigurableRetryLogicLoader"; + private const string InterfaceCnnCfgTypeName = "Microsoft.Data.SqlClient.ISqlConfigurableRetryConnectionSection"; + private const string InterfaceCmdCfgTypeName = "Microsoft.Data.SqlClient.ISqlConfigurableRetryCommandSection"; + private const string CnnCfgTypeName = "Microsoft.Data.SqlClient.SqlConfigurableRetryConnectionSection"; + private const string CmdCfgTypeName = "Microsoft.Data.SqlClient.SqlConfigurableRetryCommandSection"; + private const string DefaultTransientErrors = "1204, 1205, 1222, 49918, 49919, 49920, 4060, 4221, 40143, 40613, 40501, 40540, 40197, 42108, 42109, 10929, 10928, 10060, 10054, 10053, 997, 233, 64, 20, 0, -2, 207, 102, 2812"; + + private static readonly Random s_random = new(); + + private static readonly Assembly s_sqlClientAssembly = typeof(SqlConnection).Assembly; + private static readonly Type s_configurationLoaderType = s_sqlClientAssembly.GetType(ConfigurationLoaderTypeName)!; + private static readonly Type s_interfaceCnnCfgType = s_sqlClientAssembly.GetType(InterfaceCnnCfgTypeName)!; + private static readonly Type s_interfaceCmdCfgType = s_sqlClientAssembly.GetType(InterfaceCmdCfgTypeName)!; + private static readonly Type s_cnnCfgType = s_sqlClientAssembly.GetType(CnnCfgTypeName)!; + private static readonly Type s_cmdCfgType = s_sqlClientAssembly.GetType(CmdCfgTypeName)!; + + private static readonly Type[] s_cfgLoaderParamsType = + [ + s_interfaceCnnCfgType, + s_interfaceCmdCfgType, + typeof(string), typeof(string) + ]; + private static readonly ConstructorInfo s_loaderCtorInfo = s_configurationLoaderType.GetConstructor(s_cfgLoaderParamsType)!; + + /// + /// Creates and returns an instance of SqlConfigurableRetryLogicLoader with the specified + /// connection and command retry logic configurations. + /// + /// The configuration for the connection retry logic. + /// The configuration for the command retry logic. + /// The created loader instance. + public static object CreateLoader(RetryLogicConfigs cnnConfig, RetryLogicConfigs cmdConfig) + { + object cnnCfgObj = Activator.CreateInstance(s_cnnCfgType)!; + SetValue(cnnCfgObj, s_cnnCfgType, "DeltaTime", cnnConfig.DeltaTime); + SetValue(cnnCfgObj, s_cnnCfgType, "MinTimeInterval", cnnConfig.MinTimeInterval); + SetValue(cnnCfgObj, s_cnnCfgType, "MaxTimeInterval", cnnConfig.MaxTimeInterval); + SetValue(cnnCfgObj, s_cnnCfgType, "NumberOfTries", cnnConfig.NumberOfTries); + SetValue(cnnCfgObj, s_cnnCfgType, "AuthorizedSqlCondition", cnnConfig.AuthorizedSqlCondition); + SetValue(cnnCfgObj, s_cnnCfgType, "TransientErrors", cnnConfig.TransientErrors); + SetValue(cnnCfgObj, s_cnnCfgType, "RetryLogicType", cnnConfig.RetryLogicType); + SetValue(cnnCfgObj, s_cnnCfgType, "RetryMethod", cnnConfig.RetryMethodName); + + object cmdCfgObj = Activator.CreateInstance(s_cmdCfgType)!; + SetValue(cmdCfgObj, s_cmdCfgType, "DeltaTime", cmdConfig.DeltaTime); + SetValue(cmdCfgObj, s_cmdCfgType, "MinTimeInterval", cmdConfig.MinTimeInterval); + SetValue(cmdCfgObj, s_cmdCfgType, "MaxTimeInterval", cmdConfig.MaxTimeInterval); + SetValue(cmdCfgObj, s_cmdCfgType, "NumberOfTries", cmdConfig.NumberOfTries); + SetValue(cmdCfgObj, s_cmdCfgType, "AuthorizedSqlCondition", cmdConfig.AuthorizedSqlCondition); + SetValue(cmdCfgObj, s_cmdCfgType, "TransientErrors", cmdConfig.TransientErrors); + SetValue(cmdCfgObj, s_cmdCfgType, "RetryLogicType", cmdConfig.RetryLogicType); + SetValue(cmdCfgObj, s_cmdCfgType, "RetryMethod", cmdConfig.RetryMethodName); + + return s_loaderCtorInfo.Invoke([cnnCfgObj, cmdCfgObj, default!, default!]); + } + + /// + /// Creates and returns an instance of SqlConfigurableRetryLogicLoader with the specified + /// connection and command retry logic configurations, and also retrieves the associated + /// connection and command providers. + /// + /// The configuration for the connection retry logic. + /// The configuration for the command retry logic. + /// When this method returns, contains the connection provider. + /// When this method returns, contains the command provider. + /// The created loader instance. + public static object ReturnLoaderAndProviders(RetryLogicConfigs cnnCfg, RetryLogicConfigs cmdCfg, out SqlRetryLogicBaseProvider cnnProvider, out SqlRetryLogicBaseProvider cmdProvider) + { + object loaderObj = CreateLoader(cnnCfg, cmdCfg); + + cnnProvider = GetValue(loaderObj, s_configurationLoaderType, "ConnectionProvider"); + cmdProvider = GetValue(loaderObj, s_configurationLoaderType, "CommandProvider"); + return loaderObj; + } + + /// + /// Creates a RetryLogicConfigs object with random values for its properties, optionally specifying + /// default values for certain mandatory properties. + /// + /// The retry method to use. + /// The authorized SQL condition. + /// The transient errors. + /// The created retry logic configuration. + public static RetryLogicConfigs CreateRandomConfig(string? method, string? authorizedSqlCondition = null, string transientErrors = DefaultTransientErrors) + { + TimeSpan start = TimeSpan.Zero; + TimeSpan end = TimeSpan.FromSeconds(60); + TimeSpan min = GenerateTimeSpan(start, end); + + return new RetryLogicConfigs() + { + DeltaTime = GenerateTimeSpan(start, end), + MinTimeInterval = min, + MaxTimeInterval = GenerateTimeSpan(min, end), + NumberOfTries = s_random.Next(1, 60), + AuthorizedSqlCondition = authorizedSqlCondition, + TransientErrors = transientErrors, + RetryMethodName = method + }; + } + + private static T GetValue(object obj, Type type, string propName, BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) => + (T)type.GetProperty(propName, flags)?.GetValue(obj)!; + + private static void SetValue(object obj, Type type, string propName, T value, BindingFlags flags = BindingFlags.Public | BindingFlags.Instance) => + type.GetProperty(propName, flags)?.SetValue(obj, value); + + private static TimeSpan GenerateTimeSpan(TimeSpan start, TimeSpan end) + { + int max = (int)(end - start).TotalSeconds; + return start.Add(TimeSpan.FromSeconds(s_random.Next(max))); + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConfigurableRetryLogicTest.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConfigurableRetryLogicTest.cs index b24a32e804..450135f440 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConfigurableRetryLogicTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConfigurableRetryLogicTest.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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. @@ -86,11 +86,10 @@ public void ValidateRetryParameters() #if NET /// /// Regression test: triggering the configurable retry logic loader through its normal - /// entry points must not leave a process-wide - /// resolving handler - /// installed. Such a handler participates in resolution of every assembly the host - /// application fails to find, and serves them out of this component's probing directory, - /// which can load code from an unintended location. + /// entry points must not leave a resolving handler subscribed to the current + /// . Such a handler participates + /// in resolution of every assembly the host application fails to find, and serves them + /// out of this component's probing directory, which can load code from an unintended location. /// [Fact] public void RetryLogicProviderDoesNotLeaveAssemblyProbingEnabled() diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTests.csproj b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTests.csproj index 5fbfeb94c2..5fe219a71d 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTests.csproj @@ -103,6 +103,7 @@ + @@ -134,6 +135,7 @@ + diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/RetryLogic/RetryLogicConfigHelper.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/RetryLogic/RetryLogicConfigHelper.cs index bd6e551890..89bb478fe0 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/RetryLogic/RetryLogicConfigHelper.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/RetryLogic/RetryLogicConfigHelper.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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. @@ -6,21 +6,12 @@ using System.Collections.Generic; using System.Reflection; using Xunit; +using Microsoft.Data.SqlClient.Tests.Common.ConfigurableRetryLogic; namespace Microsoft.Data.SqlClient.ManualTesting.Tests { public class RetryLogicConfigHelper { - private const string ConfigurationLoaderTypeName = "Microsoft.Data.SqlClient.SqlConfigurableRetryLogicLoader"; - private const string AppContextSwitchManagerTypeName = "Microsoft.Data.SqlClient.SqlAppContextSwitchManager"; - private const string ApplyContextSwitchesMethodName = "ApplyContextSwitches"; - private const string InterfaceCnnCfgTypeName = "Microsoft.Data.SqlClient.ISqlConfigurableRetryConnectionSection"; - private const string InterfaceCmdCfgTypeName = "Microsoft.Data.SqlClient.ISqlConfigurableRetryCommandSection"; - - private const string CnnCfgTypeName = "Microsoft.Data.SqlClient.SqlConfigurableRetryConnectionSection"; - private const string CmdCfgTypeName = "Microsoft.Data.SqlClient.SqlConfigurableRetryCommandSection"; - private const string AppCtxCfgTypeName = "Microsoft.Data.SqlClient.AppContextSwitchOverridesSection"; - public const string RetryMethodName_Fix = "CreateFixedRetryProvider"; public const string RetryMethodName_Inc = "CreateIncrementalRetryProvider"; public const string RetryMethodName_Exp = "CreateExponentialRetryProvider"; @@ -31,79 +22,21 @@ public class RetryLogicConfigHelper private const string CreateExceptionMethodName = "CreateException"; private const string AddMethodName = "Add"; - public const string DefaultTransientErrors = "1204, 1205, 1222, 49918, 49919, 49920, 4060, 4221, 40143, 40613, 40501, 40540, 40197, 42108, 42109, 10929, 10928, 10060, 10054, 10053, 997, 233, 64, 20, 0, -2, 207, 102, 2812"; - private static readonly Random s_random = new Random(); private static readonly Assembly s_sqlClientAssembly = typeof(SqlConnection).Assembly; - private static readonly Type s_appContextSwitchManagerType = s_sqlClientAssembly.GetType(AppContextSwitchManagerTypeName); private static readonly Type s_sqlretrylogicType = s_sqlClientAssembly.GetType(SqlRetryLogicTypeName); - private static readonly Type s_configurationLoaderType = s_sqlClientAssembly.GetType(ConfigurationLoaderTypeName); private static readonly Type s_sqlErrorType = typeof(SqlError); private static readonly Type s_sqlErrorCollectionType = typeof(SqlErrorCollection); - private static readonly Type[] s_cfgLoaderParamsType = new Type[] - { - s_sqlClientAssembly.GetType(InterfaceCnnCfgTypeName), - s_sqlClientAssembly.GetType(InterfaceCmdCfgTypeName), - typeof(string), typeof(string) - }; private static readonly Type[] s_sqlErrorParamsType = new Type[] { typeof(int), typeof(byte), typeof(byte), typeof(string), typeof(string), typeof(string), typeof(int), typeof(Exception) }; - private static readonly ConstructorInfo s_loaderCtorInfo = s_configurationLoaderType.GetConstructor(s_cfgLoaderParamsType); private static readonly ConstructorInfo s_sqlErrorCtorInfo = s_sqlErrorType.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, s_sqlErrorParamsType, null); private static readonly ConstructorInfo s_sqlErrorCollectionCtorInfo = s_sqlErrorCollectionType.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, Type.EmptyTypes, null); - public static object CreateLoader(RetryLogicConfigs cnnConfig, RetryLogicConfigs cmdConfig) - { - var cnnCfgType = s_sqlClientAssembly.GetType(CnnCfgTypeName); - var cnnCfgObj = Activator.CreateInstance(cnnCfgType); - SetValue(cnnCfgObj, cnnCfgType, "DeltaTime", cnnConfig.DeltaTime); - SetValue(cnnCfgObj, cnnCfgType, "MinTimeInterval", cnnConfig.MinTimeInterval); - SetValue(cnnCfgObj, cnnCfgType, "MaxTimeInterval", cnnConfig.MaxTimeInterval); - SetValue(cnnCfgObj, cnnCfgType, "NumberOfTries", cnnConfig.NumberOfTries); - SetValue(cnnCfgObj, cnnCfgType, "AuthorizedSqlCondition", cnnConfig.AuthorizedSqlCondition); - SetValue(cnnCfgObj, cnnCfgType, "TransientErrors", cnnConfig.TransientErrors); - SetValue(cnnCfgObj, cnnCfgType, "RetryLogicType", cnnConfig.RetryLogicType); - SetValue(cnnCfgObj, cnnCfgType, "RetryMethod", cnnConfig.RetryMethodName); - - var cmdCfgType = s_sqlClientAssembly.GetType(CmdCfgTypeName); - var cmdCfgObj = Activator.CreateInstance(cmdCfgType); - SetValue(cmdCfgObj, cmdCfgType, "DeltaTime", cmdConfig.DeltaTime); - SetValue(cmdCfgObj, cmdCfgType, "MinTimeInterval", cmdConfig.MinTimeInterval); - SetValue(cmdCfgObj, cmdCfgType, "MaxTimeInterval", cmdConfig.MaxTimeInterval); - SetValue(cmdCfgObj, cmdCfgType, "NumberOfTries", cmdConfig.NumberOfTries); - SetValue(cmdCfgObj, cmdCfgType, "AuthorizedSqlCondition", cmdConfig.AuthorizedSqlCondition); - SetValue(cmdCfgObj, cmdCfgType, "TransientErrors", cmdConfig.TransientErrors); - SetValue(cmdCfgObj, cmdCfgType, "RetryLogicType", cmdConfig.RetryLogicType); - SetValue(cmdCfgObj, cmdCfgType, "RetryMethod", cmdConfig.RetryMethodName); - - return s_loaderCtorInfo.Invoke(new object[] { cnnCfgObj, cmdCfgObj, default, default }); - } - - public static void ApplyContextSwitchByManager(string name, bool value) - { -#if !NETFRAMEWORK - var appCtxType = s_sqlClientAssembly.GetType(AppCtxCfgTypeName); - var appCtxObj = Activator.CreateInstance(appCtxType); - SetValue(appCtxObj, appCtxType, "Value", string.Concat(name, "=", value)); - - var method = s_appContextSwitchManagerType.GetMethod(ApplyContextSwitchesMethodName, BindingFlags.Static | BindingFlags.NonPublic); - method.Invoke(null, new object[] { appCtxObj }); -#else - AppContext.SetSwitch(name, value); -#endif - } - - public static SqlRetryLogicBaseProvider GetConnectionProvider(object loader) - => GetValue(loader, s_configurationLoaderType, "ConnectionProvider"); - - public static SqlRetryLogicBaseProvider GetCommandProvider(object loader) - => GetValue(loader, s_configurationLoaderType, "CommandProvider"); - public static void AssessProvider(SqlRetryLogicBaseProvider provider, RetryLogicConfigs option) => AssessRetryLogic(provider.RetryLogic, option); @@ -150,33 +83,6 @@ public static IEnumerable GetIivalidTimes() } } - public static object ReturnLoaderAndProviders(RetryLogicConfigs cnnCfg, RetryLogicConfigs cmdCfg, out SqlRetryLogicBaseProvider cnnProvider, out SqlRetryLogicBaseProvider cmdProvider) - { - var loaderObj = CreateLoader(cnnCfg, cmdCfg); - cnnProvider = GetConnectionProvider(loaderObj); - cmdProvider = GetCommandProvider(loaderObj); - return loaderObj; - } - - public static RetryLogicConfigs CreateRandomConfig(string method, string authorizedSqlCondition = null, string transientErrors = DefaultTransientErrors) - { - TimeSpan start = TimeSpan.Zero; - TimeSpan end = TimeSpan.FromSeconds(60); - - var min = GenerateTimeSpan(start, end); - - return new RetryLogicConfigs() - { - DeltaTime = GenerateTimeSpan(start, end), - MinTimeInterval = min, - MaxTimeInterval = GenerateTimeSpan(min, end), - NumberOfTries = s_random.Next(1, 60), - AuthorizedSqlCondition = authorizedSqlCondition, - TransientErrors = transientErrors, - RetryMethodName = method - }; - } - public static SqlException CreateSqlException(int errorNumber) { MethodInfo addSqlErrorMethod = typeof(SqlErrorCollection).GetMethod(AddMethodName, BindingFlags.Instance | BindingFlags.NonPublic); @@ -199,21 +105,4 @@ private static TimeSpan GenerateTimeSpan(TimeSpan start, TimeSpan end) return start.Add(TimeSpan.FromSeconds(s_random.Next(max))); } } - - public class RetryLogicConfigs : ICloneable - { - public TimeSpan DeltaTime { get; set; } - public TimeSpan MaxTimeInterval { get; set; } - public TimeSpan MinTimeInterval { get; set; } - public int NumberOfTries { get; set; } - public string TransientErrors { get; set; } - public string AuthorizedSqlCondition { get; set; } - public string RetryLogicType { get; set; } - public string RetryMethodName { get; set; } - - public object Clone() - { - return MemberwiseClone(); - } - } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/RetryLogic/SqlConfigurationManagerReliabilityTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/RetryLogic/SqlConfigurationManagerReliabilityTest.cs index 8ffbbdb5bf..053c91c0b9 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/RetryLogic/SqlConfigurationManagerReliabilityTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/RetryLogic/SqlConfigurationManagerReliabilityTest.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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. @@ -6,6 +6,7 @@ using Xunit; using System.Data; using System.Threading.Tasks; +using Microsoft.Data.SqlClient.Tests.Common.ConfigurableRetryLogic; namespace Microsoft.Data.SqlClient.ManualTesting.Tests { @@ -31,15 +32,15 @@ public class SqlConfigurationManagerReliabilityTest [InlineData(RetryLogicConfigHelper.RetryMethodName_Exp, RetryLogicConfigHelper.RetryMethodName_Fix)] public void LoadValidInternalTypes(string method1, string method2) { - RetryLogicConfigs cnnCfg = RetryLogicConfigHelper.CreateRandomConfig(method1); - RetryLogicConfigs cmdCfg = RetryLogicConfigHelper.CreateRandomConfig(method2, + RetryLogicConfigs cnnCfg = RetryLogicLoaderHelper.CreateRandomConfig(method1); + RetryLogicConfigs cmdCfg = RetryLogicLoaderHelper.CreateRandomConfig(method2, // Doesn't accept DML statements @"^\b(?!UPDATE|DELETE|TRUNCATE|INSERT( +INTO){0,1})\b"); // for sake of reducing the retry time in total cnnCfg.NumberOfTries = 1; cmdCfg.NumberOfTries = 1; - object loaderObj = RetryLogicConfigHelper.ReturnLoaderAndProviders(cnnCfg, cmdCfg, out SqlRetryLogicBaseProvider cnnProvider, out SqlRetryLogicBaseProvider cmdProvider); + object loaderObj = RetryLogicLoaderHelper.ReturnLoaderAndProviders(cnnCfg, cmdCfg, out SqlRetryLogicBaseProvider cnnProvider, out SqlRetryLogicBaseProvider cmdProvider); Assert.NotNull(loaderObj); RetryLogicConfigHelper.AssessProvider(cnnProvider, cnnCfg); RetryLogicConfigHelper.AssessProvider(cmdProvider, cmdCfg); @@ -66,8 +67,8 @@ public void LoadValidInternalTypes(string method1, string method2) [InlineData("Microsoft.Data.SqlClient.Tests.CustomConfigurableRetryLogicEx, ExternalConfigurableRetryLogic", "GetDefaultRetry")] public void LoadCustomMethod(string typeName, string methodName) { - RetryLogicConfigs cnnCfg = RetryLogicConfigHelper.CreateRandomConfig(methodName); - RetryLogicConfigs cmdCfg = RetryLogicConfigHelper.CreateRandomConfig(methodName); + RetryLogicConfigs cnnCfg = RetryLogicLoaderHelper.CreateRandomConfig(methodName); + RetryLogicConfigs cmdCfg = RetryLogicLoaderHelper.CreateRandomConfig(methodName); // for sake of reducing the retry time in total cnnCfg.RetryLogicType = typeName; cmdCfg.RetryLogicType = typeName; @@ -75,7 +76,7 @@ public void LoadCustomMethod(string typeName, string methodName) cnnCfg.NumberOfTries = 1; cmdCfg.NumberOfTries = 3; - object loaderObj = RetryLogicConfigHelper.ReturnLoaderAndProviders(cnnCfg, cmdCfg, out SqlRetryLogicBaseProvider cnnProvider, out SqlRetryLogicBaseProvider cmdProvider); + object loaderObj = RetryLogicLoaderHelper.ReturnLoaderAndProviders(cnnCfg, cmdCfg, out SqlRetryLogicBaseProvider cnnProvider, out SqlRetryLogicBaseProvider cmdProvider); Assert.NotNull(loaderObj); TestConnection(cnnProvider, cnnCfg); @@ -96,8 +97,8 @@ public void LoadCustomMethod(string typeName, string methodName) [InlineData("Microsoft.Data.SqlClient.Tests.CustomConfigurableRetryLogic, ExternalConfigurableRetryLogic", "getdefaultretry")] public void LoadInvalidCustomRetryLogicType(string typeName, string methodName) { - RetryLogicConfigs cnnCfg = RetryLogicConfigHelper.CreateRandomConfig(methodName); - RetryLogicConfigs cmdCfg = RetryLogicConfigHelper.CreateRandomConfig(methodName); + RetryLogicConfigs cnnCfg = RetryLogicLoaderHelper.CreateRandomConfig(methodName); + RetryLogicConfigs cmdCfg = RetryLogicLoaderHelper.CreateRandomConfig(methodName); // for sake of reducing the retry time in total cnnCfg.RetryLogicType = typeName; cmdCfg.RetryLogicType = typeName; @@ -105,7 +106,7 @@ public void LoadInvalidCustomRetryLogicType(string typeName, string methodName) cnnCfg.NumberOfTries = 1; cmdCfg.NumberOfTries = 3; - object loaderObj = RetryLogicConfigHelper.ReturnLoaderAndProviders(cnnCfg, cmdCfg, out SqlRetryLogicBaseProvider cnnProvider, out SqlRetryLogicBaseProvider cmdProvider); + object loaderObj = RetryLogicLoaderHelper.ReturnLoaderAndProviders(cnnCfg, cmdCfg, out SqlRetryLogicBaseProvider cnnProvider, out SqlRetryLogicBaseProvider cmdProvider); Assert.NotNull(loaderObj); s_connectionCRLTest.DefaultOpenWithoutRetry(TcpCnnString, cnnProvider); @@ -121,10 +122,10 @@ public void LoadInvalidCustomRetryLogicType(string typeName, string methodName) [MemberData(nameof(RetryLogicConfigHelper.GetInvalidInternalMethodNames), MemberType = typeof(RetryLogicConfigHelper))] public void InvalidRetryMethodName(string methodName) { - RetryLogicConfigs cnnCfg = RetryLogicConfigHelper.CreateRandomConfig(methodName); - RetryLogicConfigs cmdCfg = RetryLogicConfigHelper.CreateRandomConfig(methodName, @"Don't care!"); + RetryLogicConfigs cnnCfg = RetryLogicLoaderHelper.CreateRandomConfig(methodName); + RetryLogicConfigs cmdCfg = RetryLogicLoaderHelper.CreateRandomConfig(methodName, @"Don't care!"); - object loaderObj = RetryLogicConfigHelper.ReturnLoaderAndProviders(cnnCfg, cmdCfg, out SqlRetryLogicBaseProvider cnnProvider, out SqlRetryLogicBaseProvider cmdProvider); + object loaderObj = RetryLogicLoaderHelper.ReturnLoaderAndProviders(cnnCfg, cmdCfg, out SqlRetryLogicBaseProvider cnnProvider, out SqlRetryLogicBaseProvider cmdProvider); Assert.NotNull(loaderObj); // none retriable logic applies. @@ -141,14 +142,14 @@ public void InvalidRetryMethodName(string methodName) [InlineData("Microsoft.Data.SqlClient.SqlConfigurableRetryFactory")] public void InvalidRetryLogicTypeWithValidInternalMethodName(string typeName) { - RetryLogicConfigs cnnCfg = RetryLogicConfigHelper.CreateRandomConfig(RetryLogicConfigHelper.RetryMethodName_Fix); + RetryLogicConfigs cnnCfg = RetryLogicLoaderHelper.CreateRandomConfig(RetryLogicConfigHelper.RetryMethodName_Fix); cnnCfg.RetryLogicType = typeName; - RetryLogicConfigs cmdCfg = RetryLogicConfigHelper.CreateRandomConfig(RetryLogicConfigHelper.RetryMethodName_Fix); + RetryLogicConfigs cmdCfg = RetryLogicLoaderHelper.CreateRandomConfig(RetryLogicConfigHelper.RetryMethodName_Fix); // for sake of reducing the retry time in total cnnCfg.NumberOfTries = 1; cmdCfg.NumberOfTries = 1; - object loaderObj = RetryLogicConfigHelper.ReturnLoaderAndProviders(cnnCfg, cmdCfg, out SqlRetryLogicBaseProvider cnnProvider, out SqlRetryLogicBaseProvider cmdProvider); + object loaderObj = RetryLogicLoaderHelper.ReturnLoaderAndProviders(cnnCfg, cmdCfg, out SqlRetryLogicBaseProvider cnnProvider, out SqlRetryLogicBaseProvider cmdProvider); Assert.NotNull(loaderObj); RetryLogicConfigHelper.AssessProvider(cnnProvider, cnnCfg); RetryLogicConfigHelper.AssessProvider(cmdProvider, cmdCfg); @@ -162,23 +163,23 @@ public void InvalidRetryLogicTypeWithValidInternalMethodName(string typeName) [MemberData(nameof(RetryLogicConfigHelper.GetIivalidTimes), MemberType = typeof(RetryLogicConfigHelper))] public void OutOfRangeTime(TimeSpan deltaTime, TimeSpan minTime, TimeSpan maxTime) { - RetryLogicConfigs cnnCfg = RetryLogicConfigHelper.CreateRandomConfig(RetryLogicConfigHelper.RetryMethodName_Fix); + RetryLogicConfigs cnnCfg = RetryLogicLoaderHelper.CreateRandomConfig(RetryLogicConfigHelper.RetryMethodName_Fix); cnnCfg.DeltaTime = deltaTime; - RetryLogicConfigs cmdCfg = RetryLogicConfigHelper.CreateRandomConfig(RetryLogicConfigHelper.RetryMethodName_Fix, @"Don't care!"); + RetryLogicConfigs cmdCfg = RetryLogicLoaderHelper.CreateRandomConfig(RetryLogicConfigHelper.RetryMethodName_Fix, @"Don't care!"); - var ex = Assert.Throws(() => RetryLogicConfigHelper.ReturnLoaderAndProviders(cnnCfg, cmdCfg, out SqlRetryLogicBaseProvider cnnProvider, out SqlRetryLogicBaseProvider cmdProvider)); + var ex = Assert.Throws(() => RetryLogicLoaderHelper.ReturnLoaderAndProviders(cnnCfg, cmdCfg, out SqlRetryLogicBaseProvider cnnProvider, out SqlRetryLogicBaseProvider cmdProvider)); Assert.Equal(typeof(System.Configuration.ConfigurationErrorsException), ex.InnerException?.GetType()); Assert.Equal(typeof(ArgumentException), ex.InnerException?.InnerException?.GetType()); - cnnCfg = RetryLogicConfigHelper.CreateRandomConfig(RetryLogicConfigHelper.RetryMethodName_Fix); + cnnCfg = RetryLogicLoaderHelper.CreateRandomConfig(RetryLogicConfigHelper.RetryMethodName_Fix); cnnCfg.MinTimeInterval = minTime; - ex = Assert.Throws(() => RetryLogicConfigHelper.ReturnLoaderAndProviders(cnnCfg, cmdCfg, out SqlRetryLogicBaseProvider cnnProvider, out SqlRetryLogicBaseProvider cmdProvider)); + ex = Assert.Throws(() => RetryLogicLoaderHelper.ReturnLoaderAndProviders(cnnCfg, cmdCfg, out SqlRetryLogicBaseProvider cnnProvider, out SqlRetryLogicBaseProvider cmdProvider)); Assert.Equal(typeof(System.Configuration.ConfigurationErrorsException), ex.InnerException?.GetType()); Assert.Equal(typeof(ArgumentException), ex.InnerException?.InnerException?.GetType()); - cnnCfg = RetryLogicConfigHelper.CreateRandomConfig(RetryLogicConfigHelper.RetryMethodName_Fix); + cnnCfg = RetryLogicLoaderHelper.CreateRandomConfig(RetryLogicConfigHelper.RetryMethodName_Fix); cnnCfg.MaxTimeInterval = maxTime; - ex = Assert.Throws(() => RetryLogicConfigHelper.ReturnLoaderAndProviders(cnnCfg, cmdCfg, out SqlRetryLogicBaseProvider cnnProvider, out SqlRetryLogicBaseProvider cmdProvider)); + ex = Assert.Throws(() => RetryLogicLoaderHelper.ReturnLoaderAndProviders(cnnCfg, cmdCfg, out SqlRetryLogicBaseProvider cnnProvider, out SqlRetryLogicBaseProvider cmdProvider)); Assert.Equal(typeof(System.Configuration.ConfigurationErrorsException), ex.InnerException?.GetType()); Assert.Equal(typeof(ArgumentException), ex.InnerException?.InnerException?.GetType()); } @@ -190,11 +191,11 @@ public void OutOfRangeTime(TimeSpan deltaTime, TimeSpan minTime, TimeSpan maxTim [InlineData(100)] public void InvalidNumberOfTries(int num) { - RetryLogicConfigs cnnCfg = RetryLogicConfigHelper.CreateRandomConfig(RetryLogicConfigHelper.RetryMethodName_Fix); + RetryLogicConfigs cnnCfg = RetryLogicLoaderHelper.CreateRandomConfig(RetryLogicConfigHelper.RetryMethodName_Fix); cnnCfg.NumberOfTries = num; - RetryLogicConfigs cmdCfg = RetryLogicConfigHelper.CreateRandomConfig(RetryLogicConfigHelper.RetryMethodName_Fix, @"Don't care!"); + RetryLogicConfigs cmdCfg = RetryLogicLoaderHelper.CreateRandomConfig(RetryLogicConfigHelper.RetryMethodName_Fix, @"Don't care!"); - var ex = Assert.Throws(() => RetryLogicConfigHelper.ReturnLoaderAndProviders(cnnCfg, cmdCfg, out SqlRetryLogicBaseProvider cnnProvider, out SqlRetryLogicBaseProvider cmdProvider)); + var ex = Assert.Throws(() => RetryLogicLoaderHelper.ReturnLoaderAndProviders(cnnCfg, cmdCfg, out SqlRetryLogicBaseProvider cnnProvider, out SqlRetryLogicBaseProvider cmdProvider)); Assert.Equal(typeof(System.Configuration.ConfigurationErrorsException), ex.InnerException?.GetType()); Assert.Equal(typeof(ArgumentException), ex.InnerException?.InnerException?.GetType()); } @@ -214,11 +215,11 @@ public void InvalidNumberOfTries(int num) [InlineData(@"~!@#$%^&*()_+={}[]|\""':;.>(() => RetryLogicConfigHelper.ReturnLoaderAndProviders(cnnCfg, cmdCfg, out SqlRetryLogicBaseProvider cnnProvider, out SqlRetryLogicBaseProvider cmdProvider)); + var ex = Assert.Throws(() => RetryLogicLoaderHelper.ReturnLoaderAndProviders(cnnCfg, cmdCfg, out SqlRetryLogicBaseProvider cnnProvider, out SqlRetryLogicBaseProvider cmdProvider)); Assert.Equal(typeof(System.Configuration.ConfigurationErrorsException), ex.InnerException?.GetType()); Assert.Equal(typeof(ArgumentException), ex.InnerException?.InnerException?.GetType()); } @@ -232,11 +233,11 @@ public void InvalidTransientError(string errors) public void ValidTransientError(string errors) { string[] transientErrorNumbers = errors.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - RetryLogicConfigs cnnCfg = RetryLogicConfigHelper.CreateRandomConfig(RetryLogicConfigHelper.RetryMethodName_Fix); + RetryLogicConfigs cnnCfg = RetryLogicLoaderHelper.CreateRandomConfig(RetryLogicConfigHelper.RetryMethodName_Fix); cnnCfg.TransientErrors = errors; - RetryLogicConfigs cmdCfg = RetryLogicConfigHelper.CreateRandomConfig(RetryLogicConfigHelper.RetryMethodName_Fix, @"Don't care!"); + RetryLogicConfigs cmdCfg = RetryLogicLoaderHelper.CreateRandomConfig(RetryLogicConfigHelper.RetryMethodName_Fix, @"Don't care!"); - RetryLogicConfigHelper.ReturnLoaderAndProviders(cnnCfg, cmdCfg, out SqlRetryLogicBaseProvider cnnProvider, out _); + RetryLogicLoaderHelper.ReturnLoaderAndProviders(cnnCfg, cmdCfg, out SqlRetryLogicBaseProvider cnnProvider, out _); foreach(string errorString in transientErrorNumbers) { diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Unloadability/UnloadableAssemblyLoadContextTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Unloadability/UnloadableAssemblyLoadContextTest.cs new file mode 100644 index 0000000000..bc2972244b --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Unloadability/UnloadableAssemblyLoadContextTest.cs @@ -0,0 +1,163 @@ +// 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. + +#if NET + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Loader; +using System.Threading.Tasks; +using Microsoft.Data.SqlClient.Tests.Common; +using Microsoft.Data.SqlClient.UnloadableLibrary; +using Xunit; + +namespace Microsoft.Data.SqlClient.ManualTesting.Tests.SQL.Unloadability; + +/// +/// Tests verifying SqlClient's behaviour within unloadable assembly load contexts. +/// +[Trait("Set", "2")] +[Collection("AssemblyLoadContext")] +public class UnloadableAssemblyLoadContextTest +{ + /// + /// Verifies that loading SqlClient into a secondary AssemblyLoadContext does not block that + /// AssemblyLoadContext from being unloaded after use. + /// + /// A task representing the asynchronous operation. + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsTCPConnStringSetup))] + public async Task SecondaryAssemblyLoadContext_Unloads() + { + string connStr = DataTestUtility.TCPConnectionString; + string typeName = typeof(EntryPoint).FullName!; + string libraryPath = typeof(EntryPoint).Assembly.Location; + + WeakReference alcWeakReference = await LoadAndUnloadAssemblyLoadContext(typeName, libraryPath, connStr); + + for (int i = 0; i < 10 && alcWeakReference.IsAlive; i++) + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + } + + Assert.False(alcWeakReference.IsAlive); + } + + /// + /// Creates an unloadable , then acts within it to open a SqlConnection + /// and to execute a command. + /// + /// The full name of the entry point type to load into the AssemblyLoadContext. + /// The path to the assembly containing the entry point type. + /// The connection to open. + /// A to the AssemblyLoadContext. + /// + /// This method makes use of reflection to avoid accidentally creating a hard reference to the + /// secondary ALC's type definitions. It is also marked as + /// in order to ensure that this weak reference is guaranteed to be out of scope in the method's caller. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task LoadAndUnloadAssemblyLoadContext(string typeName, string assemblyPath, string connectionString) + { + // This method loads the entry point type into a new, collectible AssemblyLoadContext and + // verifies that the entry point type is referencing the SqlConnection type in the correct + // AssemblyLoadContext. + // It then manually instantiates this entry point type, and invokes GetDate and GetDateAsync. + UnloadableAssemblyLoadContext alc = new(nameof(SecondaryAssemblyLoadContext_Unloads), assemblyPath); + WeakReference weakRef = new(alc, trackResurrection: true); + Assembly loadedAsm = alc.LoadFromAssemblyPath(assemblyPath); + + Type? unloadableLibraryType = loadedAsm.GetType(typeName); + Assert.NotNull(unloadableLibraryType); + + PropertyInfo? alcNameProperty = unloadableLibraryType.GetProperty(nameof(EntryPoint.AssemblyLoadContextName), BindingFlags.Public | BindingFlags.Static); + Assert.NotNull(alcNameProperty); + string? alcName = alcNameProperty.GetValue(null) as string; + + Assert.Equal(alc.Name, alcName); + + object? instantiated = Activator.CreateInstance(unloadableLibraryType, [connectionString]); + Assert.NotNull(instantiated); + + try + { + MethodInfo? getDateMethod = unloadableLibraryType.GetMethod(nameof(EntryPoint.GetDate)); + Assert.NotNull(getDateMethod); + + getDateMethod.Invoke(instantiated, null); + + MethodInfo? getDateAsyncMethod = unloadableLibraryType.GetMethod(nameof(EntryPoint.GetDateAsync)); + Assert.NotNull(getDateAsyncMethod); + + Task? getDateTask = getDateAsyncMethod.Invoke(instantiated, null) as Task; + Assert.NotNull(getDateTask); + + await getDateTask; + } + finally + { + MethodInfo? disposeMethod = unloadableLibraryType.GetMethod(nameof(EntryPoint.Dispose)); + Assert.NotNull(disposeMethod); + + disposeMethod.Invoke(instantiated, null); + + alc.Unload(); + } + + return weakRef; + } + + /// + /// Defines an unloadable that resolves assemblies + /// using an . + /// + private sealed class UnloadableAssemblyLoadContext : AssemblyLoadContext + { + private readonly AssemblyDependencyResolver _resolver; + + /// + /// Instantiates a new unloadable with the specified + /// name and assembly path. + /// + /// Name of this . + /// Path to the assembly. + public UnloadableAssemblyLoadContext(string name, string assemblyPath) + : base(name, isCollectible: true) + { + _resolver = new AssemblyDependencyResolver(assemblyPath); + } + + /// + /// Resolves assemblies using the + /// and loads them into this . + /// + /// The object that describes the assembly to be resolved. + /// The resolved assembly, or null if it could not be resolved. + protected override Assembly? Load(AssemblyName assemblyName) + { + string? resolvedAssemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); + + return resolvedAssemblyPath is not null + ? LoadFromAssemblyPath(resolvedAssemblyPath) + : null; + } + } +} + +/// +/// Defines a test collection that serializes execution of test classes +/// which load and unload s. +/// +[CollectionDefinition("AssemblyLoadContext")] +public class UnloadableAssemblyLoadContextCollection +{ +} + + +#endif diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlConfigurableRetryLogicLoaderTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlConfigurableRetryLogicLoaderTest.cs index d4ef506991..6ab6c9dc68 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlConfigurableRetryLogicLoaderTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlConfigurableRetryLogicLoaderTest.cs @@ -15,8 +15,7 @@ namespace Microsoft.Data.SqlClient.UnitTests; /// /// Unit tests validating that never leaves a -/// process-wide assembly resolving handler attached to -/// . +/// process-wide assembly resolving handler attached to the current . /// /// /// A handler left attached there participates in resolution of every assembly the host @@ -29,7 +28,7 @@ public class SqlConfigurableRetryLogicLoaderTest { /// /// The default code path: no configuration at all. The loader must not subscribe to the - /// default load context. + /// current load context. /// [Fact] public void Constructor_WithNoConfiguration_DoesNotLeaveAssemblyProbingEnabled() @@ -228,8 +227,8 @@ private static TestRetryConnectionSection CreateSection(string? retryLogicType) /// /// Asserts that a failed assembly load is not served out of the loader's probing directory, /// which can only happen while a resolving handler installed by - /// is subscribed to - /// . + /// is subscribed to the current + /// . /// /// /// A file that is not a valid assembly is planted in the probing directory under a name no @@ -259,7 +258,7 @@ private static void AssertNoAssemblyProbingHandlerInstalled() /// /// Reports whether a resolving handler that serves assemblies out of the loader's probing - /// directory is currently subscribed to . + /// directory is currently subscribed to the current . /// /// /// This distinguishes the two states using only public behaviour, which is what actually diff --git a/src/Microsoft.Data.SqlClient/tests/UnloadableLibrary/EntryPoint.cs b/src/Microsoft.Data.SqlClient/tests/UnloadableLibrary/EntryPoint.cs new file mode 100644 index 0000000000..e65a448e8f --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnloadableLibrary/EntryPoint.cs @@ -0,0 +1,102 @@ +// 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 Microsoft.Data.SqlClient.Tests.Common; +using Microsoft.Data.SqlClient.Tests.Common.ConfigurableRetryLogic; + +namespace Microsoft.Data.SqlClient.UnloadableLibrary; + +/// +/// Entry point and hooks for the unloadable assembly load context test. +/// +/// +/// This is organised into a separate class and assembly to provide isolation from the ManualTesting +/// project. Loading an assembly loads all of its dependencies, so this isolation means that if the +/// test fails, we can be confident that the failure is due to SqlClient itself rather than to another +/// assembly that was loaded into the same context. +/// +public sealed class EntryPoint : IDisposable +{ + private readonly LocalAppContextSwitchesHelper _appContextSwitchesHelper; + + public static string? AssemblyLoadContextName => + #if NET + System.Runtime.Loader.AssemblyLoadContext.GetLoadContext(typeof(SqlConnection).Assembly)?.Name; + #else + "Default"; + #endif + + public string ConnectionString { get; } + + /// + /// Instantiates the entry point, specifying the connection string to use. + /// + /// + /// This sets up the AppContext switch interface and forces the configurable retry logic loader + /// to be created, triggering the loading of SqlClient into the current AssemblyLoadContext. + /// + /// Connection string to use for the test. + public EntryPoint(string connectionString) + { + _appContextSwitchesHelper = new LocalAppContextSwitchesHelper(); + #if NET + _appContextSwitchesHelper.UseManagedNetworking = true; + #endif + + ConnectionString = connectionString; + + InitializeRetryProviders(); + } + + private void InitializeRetryProviders() + { + RetryLogicConfigs connectionConfigs = new() + { + DeltaTime = TimeSpan.FromSeconds(1), + MaxTimeInterval = TimeSpan.FromSeconds(30), + MinTimeInterval = TimeSpan.FromSeconds(1), + NumberOfTries = 1, + RetryLogicType = typeof(SqlConfigurableRetryFactory).AssemblyQualifiedName, + RetryMethodName = nameof(SqlConfigurableRetryFactory.CreateNoneRetryProvider) + }; + RetryLogicConfigs commandConfigs = RetryLogicLoaderHelper.CreateRandomConfig(method: null, authorizedSqlCondition: null); + + RetryLogicLoaderHelper.CreateLoader(connectionConfigs, commandConfigs); + } + + /// + /// Executes a simple SQL command to retrieve the current date and time from the database. + /// + /// + public void GetDate() + { + using SqlConnection conn = new(ConnectionString); + using SqlCommand cmd = new("SELECT GETDATE()", conn); + + conn.Open(); + cmd.ExecuteScalar(); + } + + /// + /// Asynchronously executes a simple SQL command to retrieve the current date and time from the database. + /// + /// A task representing the asynchronous operation. + /// + public async Task GetDateAsync() + { + using SqlConnection conn = new(ConnectionString); + using SqlCommand cmd = new("SELECT GETDATE()", conn); + + await conn.OpenAsync().ConfigureAwait(false); + await cmd.ExecuteScalarAsync().ConfigureAwait(false); + } + + /// + /// Disposes of the resources used by the entry point, including the AppContext switch helper. + /// + public void Dispose() + { + _appContextSwitchesHelper.Dispose(); + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/UnloadableLibrary/UnloadableLibrary.csproj b/src/Microsoft.Data.SqlClient/tests/UnloadableLibrary/UnloadableLibrary.csproj new file mode 100644 index 0000000000..3b045a72b5 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnloadableLibrary/UnloadableLibrary.csproj @@ -0,0 +1,25 @@ + + + UnloadableLibrary + + enable + enable + + false + + + + + net462;net10.0 + + + + + + + + + + \ No newline at end of file