mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-01-14 01:25:40 -05:00
* Move unused forked code to external directory * Fix SLN build errors * Add back resource provider core since it's referenced by main resource provider project * Update PackageProjects step of pipeline
611 lines
24 KiB
C#
611 lines
24 KiB
C#
//
|
|
// Copyright (c) Microsoft. All rights reserved.
|
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
//
|
|
|
|
// This code is copied from the source described in the comment below.
|
|
|
|
// =======================================================================================
|
|
// Microsoft Windows Server AppFabric Customer Advisory Team (CAT) Best Practices Series
|
|
//
|
|
// This sample is supplemental to the technical guidance published on the community
|
|
// blog at http://blogs.msdn.com/appfabriccat/ and copied from
|
|
// sqlmain ./sql/manageability/mfx/common/
|
|
//
|
|
// =======================================================================================
|
|
// Copyright © 2012 Microsoft Corporation. All rights reserved.
|
|
//
|
|
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
|
|
// EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF
|
|
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. YOU BEAR THE RISK OF USING IT.
|
|
// =======================================================================================
|
|
|
|
// namespace Microsoft.AppFabricCAT.Samples.Azure.TransientFaultHandling.SqlAzure
|
|
// namespace Microsoft.SqlServer.Management.Common
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Data;
|
|
using System.Data.Common;
|
|
using Microsoft.Data.SqlClient;
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.SqlTools.Hosting.Utility;
|
|
|
|
namespace Microsoft.SqlTools.CoreServices.Connection.ReliableConnection
|
|
{
|
|
/// <summary>
|
|
/// Provides a reliable way of opening connections to and executing commands
|
|
/// taking into account potential network unreliability and a requirement for connection retry.
|
|
/// </summary>
|
|
internal sealed partial class ReliableSqlConnection : DbConnection, IDisposable
|
|
{
|
|
private const string QueryAzureSessionId = "SELECT CONVERT(NVARCHAR(36), CONTEXT_INFO())";
|
|
|
|
private readonly SqlConnection _underlyingConnection;
|
|
private readonly RetryPolicy _connectionRetryPolicy;
|
|
private RetryPolicy _commandRetryPolicy;
|
|
private Guid _azureSessionId;
|
|
private bool _isSqlDwDatabase;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the ReliableSqlConnection class with a given connection string
|
|
/// and a policy defining whether to retry a request if the connection fails to be opened or a command
|
|
/// fails to be successfully executed.
|
|
/// </summary>
|
|
/// <param name="connectionString">The connection string used to open the SQL Azure database.</param>
|
|
/// <param name="connectionRetryPolicy">The retry policy defining whether to retry a request if a connection fails to be established.</param>
|
|
/// <param name="commandRetryPolicy">The retry policy defining whether to retry a request if a command fails to be executed.</param>
|
|
public ReliableSqlConnection(string connectionString, RetryPolicy connectionRetryPolicy, RetryPolicy commandRetryPolicy)
|
|
{
|
|
_underlyingConnection = new SqlConnection(connectionString);
|
|
_connectionRetryPolicy = connectionRetryPolicy ?? RetryPolicyFactory.CreateNoRetryPolicy();
|
|
_commandRetryPolicy = commandRetryPolicy ?? RetryPolicyFactory.CreateNoRetryPolicy();
|
|
|
|
_underlyingConnection.StateChange += OnConnectionStateChange;
|
|
_connectionRetryPolicy.RetryOccurred += RetryConnectionCallback;
|
|
_commandRetryPolicy.RetryOccurred += RetryCommandCallback;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs application-defined tasks associated with freeing, releasing, or
|
|
/// resetting managed and unmanaged resources.
|
|
/// </summary>
|
|
/// <param name="disposing">A flag indicating that managed resources must be released.</param>
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
{
|
|
if (_connectionRetryPolicy != null)
|
|
{
|
|
_connectionRetryPolicy.RetryOccurred -= RetryConnectionCallback;
|
|
}
|
|
|
|
if (_commandRetryPolicy != null)
|
|
{
|
|
_commandRetryPolicy.RetryOccurred -= RetryCommandCallback;
|
|
}
|
|
|
|
_underlyingConnection.StateChange -= OnConnectionStateChange;
|
|
if (_underlyingConnection.State == ConnectionState.Open)
|
|
{
|
|
_underlyingConnection.Close();
|
|
}
|
|
|
|
_underlyingConnection.Dispose();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines if a connection is being made to a SQL DW database.
|
|
/// </summary>
|
|
/// <param name="conn">A connection object.</param>
|
|
private bool IsSqlDwConnection(IDbConnection conn)
|
|
{
|
|
//Set the connection only if it has not been set earlier.
|
|
//This is assuming that it is highly unlikely for a connection to change between instances.
|
|
//Hence any subsequent calls to this method will just return the cached value and not
|
|
//verify again if this is a SQL DW database connection or not.
|
|
if (!CachedServerInfo.Instance.TryGetIsSqlDw(conn, out _isSqlDwDatabase))
|
|
{
|
|
_isSqlDwDatabase = ReliableConnectionHelper.IsSqlDwDatabase(conn);
|
|
CachedServerInfo.Instance.AddOrUpdateIsSqlDw(conn, _isSqlDwDatabase);;
|
|
}
|
|
|
|
return _isSqlDwDatabase;
|
|
}
|
|
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities")]
|
|
internal static void SetLockAndCommandTimeout(IDbConnection conn)
|
|
{
|
|
Validate.IsNotNull(nameof(conn), conn);
|
|
|
|
// Make sure we use the underlying connection as ReliableConnection.Open also calls
|
|
// this method
|
|
ReliableSqlConnection reliableConn = conn as ReliableSqlConnection;
|
|
if (reliableConn != null)
|
|
{
|
|
conn = reliableConn._underlyingConnection;
|
|
}
|
|
|
|
const string setLockTimeout = @"set LOCK_TIMEOUT {0}";
|
|
|
|
using (IDbCommand cmd = conn.CreateCommand())
|
|
{
|
|
cmd.CommandText = string.Format(CultureInfo.InvariantCulture, setLockTimeout, AmbientSettings.LockTimeoutMilliSeconds);
|
|
cmd.CommandType = CommandType.Text;
|
|
cmd.CommandTimeout = CachedServerInfo.Instance.GetQueryTimeoutSeconds(conn);
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
}
|
|
|
|
internal static void SetDefaultAnsiSettings(IDbConnection conn, bool isSqlDw)
|
|
{
|
|
Validate.IsNotNull(nameof(conn), conn);
|
|
|
|
// Make sure we use the underlying connection as ReliableConnection.Open also calls
|
|
// this method
|
|
ReliableSqlConnection reliableConn = conn as ReliableSqlConnection;
|
|
if (reliableConn != null)
|
|
{
|
|
conn = reliableConn._underlyingConnection;
|
|
}
|
|
|
|
// Configure the connection with proper ANSI settings and lock timeout
|
|
using (IDbCommand cmd = conn.CreateCommand())
|
|
{
|
|
cmd.CommandTimeout = CachedServerInfo.Instance.GetQueryTimeoutSeconds(conn);
|
|
if (!isSqlDw)
|
|
{
|
|
cmd.CommandText = @"SET ANSI_NULLS, ANSI_PADDING, ANSI_WARNINGS, ARITHABORT, CONCAT_NULL_YIELDS_NULL, QUOTED_IDENTIFIER ON;
|
|
SET NUMERIC_ROUNDABORT OFF;";
|
|
}
|
|
else
|
|
{
|
|
cmd.CommandText = @"SET ANSI_NULLS ON; SET ANSI_PADDING ON; SET ANSI_WARNINGS ON; SET ARITHABORT ON; SET CONCAT_NULL_YIELDS_NULL ON; SET QUOTED_IDENTIFIER ON;"; //SQL DW does not support NUMERIC_ROUNDABORT
|
|
}
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the connection string for opening a connection to the SQL Azure database.
|
|
/// </summary>
|
|
public override string ConnectionString
|
|
{
|
|
get { return _underlyingConnection.ConnectionString; }
|
|
set { _underlyingConnection.ConnectionString = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the policy which decides whether to retry a connection request, based on how many
|
|
/// times the request has been made and the reason for the last failure.
|
|
/// </summary>
|
|
public RetryPolicy ConnectionRetryPolicy
|
|
{
|
|
get { return _connectionRetryPolicy; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the policy which decides whether to retry a command, based on how many
|
|
/// times the request has been made and the reason for the last failure.
|
|
/// </summary>
|
|
public RetryPolicy CommandRetryPolicy
|
|
{
|
|
get { return _commandRetryPolicy; }
|
|
set
|
|
{
|
|
Validate.IsNotNull(nameof(value), value);
|
|
|
|
if (_commandRetryPolicy != null)
|
|
{
|
|
_commandRetryPolicy.RetryOccurred -= RetryCommandCallback;
|
|
}
|
|
|
|
_commandRetryPolicy = value;
|
|
_commandRetryPolicy.RetryOccurred += RetryCommandCallback;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the server name from the underlying connection.
|
|
/// </summary>
|
|
public override string DataSource
|
|
{
|
|
get { return _underlyingConnection.DataSource; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the server version from the underlying connection.
|
|
/// </summary>
|
|
public override string ServerVersion
|
|
{
|
|
get { return _underlyingConnection.ServerVersion; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// If the underlying SqlConnection absolutely has to be accessed, for instance
|
|
/// to pass to external APIs that require this type of connection, then this
|
|
/// can be used.
|
|
/// </summary>
|
|
/// <returns><see cref="SqlConnection"/></returns>
|
|
public SqlConnection GetUnderlyingConnection()
|
|
{
|
|
return _underlyingConnection;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Begins a database transaction with the specified System.Data.IsolationLevel value.
|
|
/// </summary>
|
|
/// <param name="level">One of the System.Data.IsolationLevel values.</param>
|
|
/// <returns>An object representing the new transaction.</returns>
|
|
protected override DbTransaction BeginDbTransaction(IsolationLevel level)
|
|
{
|
|
return _underlyingConnection.BeginTransaction(level);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Changes the current database for an open Connection object.
|
|
/// </summary>
|
|
/// <param name="databaseName">The name of the database to use in place of the current database.</param>
|
|
public override void ChangeDatabase(string databaseName)
|
|
{
|
|
_underlyingConnection.ChangeDatabase(databaseName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Opens a database connection with the settings specified by the ConnectionString
|
|
/// property of the provider-specific Connection object.
|
|
/// </summary>
|
|
public override void Open()
|
|
{
|
|
// Check if retry policy was specified, if not, disable retries by executing the Open method using RetryPolicy.NoRetry.
|
|
_connectionRetryPolicy.ExecuteAction(() =>
|
|
{
|
|
if (_underlyingConnection.State != ConnectionState.Open)
|
|
{
|
|
_underlyingConnection.Open();
|
|
}
|
|
SetLockAndCommandTimeout(_underlyingConnection);
|
|
SetDefaultAnsiSettings(_underlyingConnection, IsSqlDwConnection(_underlyingConnection));
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Opens a database connection with the settings specified by the ConnectionString
|
|
/// property of the provider-specific Connection object.
|
|
/// </summary>
|
|
public override Task OpenAsync(CancellationToken token)
|
|
{
|
|
// Make sure that the token isn't cancelled before we try
|
|
if (token.IsCancellationRequested)
|
|
{
|
|
return Task.FromCanceled(token);
|
|
}
|
|
|
|
// Check if retry policy was specified, if not, disable retries by executing the Open method using RetryPolicy.NoRetry.
|
|
try
|
|
{
|
|
return _connectionRetryPolicy.ExecuteAction(async () =>
|
|
{
|
|
if (_underlyingConnection.State != ConnectionState.Open)
|
|
{
|
|
await _underlyingConnection.OpenAsync(token);
|
|
}
|
|
SetLockAndCommandTimeout(_underlyingConnection);
|
|
SetDefaultAnsiSettings(_underlyingConnection, IsSqlDwConnection(_underlyingConnection));
|
|
});
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return Task.FromException(e);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Closes the connection to the database.
|
|
/// </summary>
|
|
public override void Close()
|
|
{
|
|
_underlyingConnection.Close();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the time to wait while trying to establish a connection before terminating
|
|
/// the attempt and generating an error.
|
|
/// </summary>
|
|
public override int ConnectionTimeout
|
|
{
|
|
get { return _underlyingConnection.ConnectionTimeout; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates and returns an object implementing the IDbCommand interface which is associated
|
|
/// with the underlying SqlConnection.
|
|
/// </summary>
|
|
/// <returns>A <see cref="IDbCommand"/> object.</returns>
|
|
protected override DbCommand CreateDbCommand()
|
|
{
|
|
return CreateReliableCommand();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates and returns an object implementing the IDbCommand interface which is associated
|
|
/// with the underlying SqlConnection.
|
|
/// </summary>
|
|
/// <returns>A <see cref="SqlCommand"/> object.</returns>
|
|
public SqlCommand CreateSqlCommand()
|
|
{
|
|
return _underlyingConnection.CreateCommand();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the name of the current database or the database to be used after a
|
|
/// connection is opened.
|
|
/// </summary>
|
|
public override string Database
|
|
{
|
|
get { return _underlyingConnection.Database; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the current state of the connection.
|
|
/// </summary>
|
|
public override ConnectionState State
|
|
{
|
|
get { return _underlyingConnection.State; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds an info message event listener.
|
|
/// </summary>
|
|
/// <param name="handler">An info message event listener.</param>
|
|
public void AddInfoMessageHandler(SqlInfoMessageEventHandler handler)
|
|
{
|
|
_underlyingConnection.InfoMessage += handler;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes an info message event listener.
|
|
/// </summary>
|
|
/// <param name="handler">An info message event listener.</param>
|
|
public void RemoveInfoMessageHandler(SqlInfoMessageEventHandler handler)
|
|
{
|
|
_underlyingConnection.InfoMessage -= handler;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears underlying connection pool.
|
|
/// </summary>
|
|
public void ClearPool()
|
|
{
|
|
if (_underlyingConnection != null)
|
|
{
|
|
SqlConnection.ClearPool(_underlyingConnection);
|
|
}
|
|
}
|
|
|
|
private void RetryCommandCallback(RetryState retryState)
|
|
{
|
|
RetryPolicyUtils.RaiseSchemaAmbientRetryMessage(retryState, SqlSchemaModelErrorCodes.ServiceActions.CommandRetry, _azureSessionId);
|
|
}
|
|
|
|
private void RetryConnectionCallback(RetryState retryState)
|
|
{
|
|
RetryPolicyUtils.RaiseSchemaAmbientRetryMessage(retryState, SqlSchemaModelErrorCodes.ServiceActions.ConnectionRetry, _azureSessionId);
|
|
}
|
|
|
|
public void OnConnectionStateChange(object sender, StateChangeEventArgs e)
|
|
{
|
|
SqlConnection conn = (SqlConnection)sender;
|
|
switch (e.CurrentState)
|
|
{
|
|
case ConnectionState.Open:
|
|
RetrieveSessionId();
|
|
break;
|
|
case ConnectionState.Broken:
|
|
case ConnectionState.Closed:
|
|
_azureSessionId = Guid.Empty;
|
|
break;
|
|
case ConnectionState.Connecting:
|
|
case ConnectionState.Executing:
|
|
case ConnectionState.Fetching:
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void RetrieveSessionId()
|
|
{
|
|
try
|
|
{
|
|
using (IDbCommand command = CreateReliableCommand())
|
|
{
|
|
IDbConnection connection = command.Connection;
|
|
if (!IsSqlDwConnection(connection))
|
|
{
|
|
command.CommandText = QueryAzureSessionId;
|
|
object result = command.ExecuteScalar();
|
|
|
|
// Only returns a session id for SQL Azure
|
|
if (DBNull.Value != result)
|
|
{
|
|
string sessionId = (string)command.ExecuteScalar();
|
|
_azureSessionId = new Guid(sessionId);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (SqlException exception)
|
|
{
|
|
Logger.Write(TraceEventType.Error, Resources.UnableToRetrieveAzureSessionId + exception.ToString());
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates and returns a ReliableSqlCommand object associated
|
|
/// with the underlying SqlConnection.
|
|
/// </summary>
|
|
/// <returns>A <see cref="ReliableSqlCommand"/> object.</returns>
|
|
private ReliableSqlCommand CreateReliableCommand()
|
|
{
|
|
return new ReliableSqlCommand(this);
|
|
}
|
|
|
|
private void VerifyConnectionOpen(IDbCommand command)
|
|
{
|
|
// Verify whether or not the connection is valid and is open. This code may be retried therefore
|
|
// it is important to ensure that a connection is re-established should it have previously failed.
|
|
if (command.Connection == null)
|
|
{
|
|
command.Connection = this;
|
|
}
|
|
|
|
if (command.Connection.State != ConnectionState.Open)
|
|
{
|
|
SqlConnection.ClearPool(_underlyingConnection);
|
|
|
|
command.Connection.Open();
|
|
}
|
|
}
|
|
|
|
private IDataReader ExecuteReader(IDbCommand command, CommandBehavior behavior)
|
|
{
|
|
Tuple<string, bool>[] sessionSettings = null;
|
|
return _commandRetryPolicy.ExecuteAction<IDataReader>(() =>
|
|
{
|
|
VerifyConnectionOpen(command);
|
|
sessionSettings = CacheOrReplaySessionSettings(command, sessionSettings);
|
|
|
|
return command.ExecuteReader(behavior);
|
|
});
|
|
}
|
|
|
|
// Because retry loses session settings, cache session settings or reply if the settings are already cached.
|
|
internal Tuple<string, bool>[] CacheOrReplaySessionSettings(IDbCommand originalCommand, Tuple<string, bool>[] sessionSettings)
|
|
{
|
|
if (sessionSettings == null)
|
|
{
|
|
sessionSettings = QuerySessionSettings(originalCommand);
|
|
}
|
|
else
|
|
{
|
|
SetSessionSettings(originalCommand.Connection, sessionSettings);
|
|
}
|
|
|
|
return sessionSettings;
|
|
}
|
|
|
|
private object ExecuteScalar(IDbCommand command)
|
|
{
|
|
Tuple<string,bool>[] sessionSettings = null;
|
|
return _commandRetryPolicy.ExecuteAction(() =>
|
|
{
|
|
VerifyConnectionOpen(command);
|
|
sessionSettings = CacheOrReplaySessionSettings(command, sessionSettings);
|
|
|
|
return command.ExecuteScalar();
|
|
});
|
|
}
|
|
|
|
private Tuple<string, bool>[] QuerySessionSettings(IDbCommand originalCommand)
|
|
{
|
|
Tuple<string,bool>[] sessionSettings = new Tuple<string,bool>[2];
|
|
|
|
IDbConnection connection = originalCommand.Connection;
|
|
if (IsSqlDwConnection(connection))
|
|
{
|
|
// SESSIONPROPERTY is not supported. Use default values for now
|
|
sessionSettings[0] = Tuple.Create("ANSI_NULLS", true);
|
|
sessionSettings[1] = Tuple.Create("QUOTED_IDENTIFIER", true);
|
|
}
|
|
else
|
|
{
|
|
using (IDbCommand localCommand = connection.CreateCommand())
|
|
{
|
|
// Executing a reader requires preservation of any pending transaction created by the calling command
|
|
localCommand.Transaction = originalCommand.Transaction;
|
|
localCommand.CommandText = "SELECT ISNULL(SESSIONPROPERTY ('ANSI_NULLS'), 0), ISNULL(SESSIONPROPERTY ('QUOTED_IDENTIFIER'), 1)";
|
|
using (IDataReader reader = localCommand.ExecuteReader())
|
|
{
|
|
if (reader.Read())
|
|
{
|
|
sessionSettings[0] = Tuple.Create("ANSI_NULLS", ((int)reader[0] == 1));
|
|
sessionSettings[1] = Tuple.Create("QUOTED_IDENTIFIER", ((int)reader[1] ==1));
|
|
}
|
|
else
|
|
{
|
|
Debug.Assert(false, "Reader cannot be empty");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return sessionSettings;
|
|
}
|
|
|
|
private void SetSessionSettings(IDbConnection connection, params Tuple<string, bool>[] settings)
|
|
{
|
|
List<string> setONOptions = new List<string>();
|
|
List<string> setOFFOptions = new List<string>();
|
|
if(settings != null)
|
|
{
|
|
foreach (Tuple<string, bool> setting in settings)
|
|
{
|
|
if (setting.Item2)
|
|
{
|
|
setONOptions.Add(setting.Item1);
|
|
}
|
|
else
|
|
{
|
|
setOFFOptions.Add(setting.Item1);
|
|
}
|
|
}
|
|
}
|
|
|
|
SetSessionSettings(connection, setONOptions, "ON");
|
|
SetSessionSettings(connection, setOFFOptions, "OFF");
|
|
|
|
}
|
|
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities")]
|
|
private static void SetSessionSettings(IDbConnection connection, List<string> sessionOptions, string onOff)
|
|
{
|
|
if (sessionOptions.Count > 0)
|
|
{
|
|
using (IDbCommand localCommand = connection.CreateCommand())
|
|
{
|
|
StringBuilder builder = new StringBuilder("SET ");
|
|
for (int i = 0; i < sessionOptions.Count; i++)
|
|
{
|
|
if (i > 0)
|
|
{
|
|
builder.Append(',');
|
|
}
|
|
builder.Append(sessionOptions[i]);
|
|
}
|
|
builder.Append(" ");
|
|
builder.Append(onOff);
|
|
localCommand.CommandText = builder.ToString();
|
|
localCommand.ExecuteNonQuery();
|
|
}
|
|
}
|
|
}
|
|
|
|
private int ExecuteNonQuery(IDbCommand command)
|
|
{
|
|
Tuple<string, bool>[] sessionSettings = null;
|
|
return _commandRetryPolicy.ExecuteAction<int>(() =>
|
|
{
|
|
VerifyConnectionOpen(command);
|
|
sessionSettings = CacheOrReplaySessionSettings(command, sessionSettings);
|
|
|
|
return command.ExecuteNonQuery();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|