//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Security;
using Microsoft.SqlTools.ServiceLayer.Utility;
namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection
{
internal static class ReliableConnectionHelper
{
private const int PCU1BuildNumber = 2816;
public readonly static SqlConnectionStringBuilder BuilderWithDefaultApplicationName = new SqlConnectionStringBuilder("server=(local);");
private const string ServerNameLocalhost = "localhost";
private const string SqlProviderName = "System.Data.SqlClient";
private const string ApplicationIntent = "ApplicationIntent";
private const string MultiSubnetFailover = "MultiSubnetFailover";
private const string DacFxApplicationName = "DacFx";
// See MSDN documentation for "SERVERPROPERTY (SQL Azure Database)" for "EngineEdition" property:
// http://msdn.microsoft.com/en-us/library/ee336261.aspx
private const int SqlAzureEngineEditionId = 5;
///
/// Opens the connection and sets the lock/command timeout and pooling=false.
///
/// The opened connection
public static IDbConnection OpenConnection(SqlConnectionStringBuilder csb, bool useRetry)
{
csb.Pooling = false;
return OpenConnection(csb.ToString(), useRetry);
}
///
/// Opens the connection and sets the lock/command timeout. This routine
/// will assert if pooling!=false.
///
/// The opened connection
public static IDbConnection OpenConnection(string connectionString, bool useRetry)
{
#if DEBUG
try
{
SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(connectionString);
Debug.Assert(builder.Pooling == false, "Pooling should be false");
}
catch (Exception ex)
{
Debug.Assert(false, "Invalid connectionstring: " + ex.Message);
}
#endif
if (AmbientSettings.AlwaysRetryOnTransientFailure == true)
{
useRetry = true;
}
RetryPolicy commandRetryPolicy, connectionRetryPolicy;
if (useRetry)
{
commandRetryPolicy = RetryPolicyFactory.CreateDefaultSchemaCommandRetryPolicy(useRetry: true);
connectionRetryPolicy = RetryPolicyFactory.CreateDefaultSchemaConnectionRetryPolicy();
}
else
{
commandRetryPolicy = RetryPolicyFactory.CreateNoRetryPolicy();
connectionRetryPolicy = RetryPolicyFactory.CreateNoRetryPolicy();
}
ReliableSqlConnection connection = new ReliableSqlConnection(connectionString, connectionRetryPolicy, commandRetryPolicy);
try
{
connection.Open();
}
catch (Exception ex)
{
string debugMessage = String.Format(CultureInfo.CurrentCulture,
"Opening connection using connection string '{0}' failed with exception: {1}", connectionString, ex.Message);
#if DEBUG
Debug.WriteLine(debugMessage);
#endif
connection.Dispose();
throw;
}
return connection;
}
///
/// Opens the connection (if it is not already) and sets
/// the lock/command timeout.
///
/// The connection to open
public static void OpenConnection(IDbConnection conn)
{
if (conn.State == ConnectionState.Closed)
{
conn.Open();
}
}
///
/// Opens a connection using 'csb' as the connection string. Provide
/// 'usingConnection' to execute T-SQL against the open connection and
/// 'catchException' to handle errors.
///
/// The connection string used when opening the IDbConnection
/// delegate called when the IDbConnection has been successfully opened
/// delegate called when an exception has occurred. Pass back 'true' to handle the
/// exception, 'false' to throw. If Null is passed in then all exceptions are thrown.
/// Should retry logic be used when opening the connection
public static void OpenConnection(
SqlConnectionStringBuilder csb,
Action usingConnection,
Predicate catchException,
bool useRetry)
{
Validate.IsNotNull(nameof(csb), csb);
Validate.IsNotNull(nameof(usingConnection), usingConnection);
try
{
// Always disable pooling
csb.Pooling = false;
using (IDbConnection conn = OpenConnection(csb.ConnectionString, useRetry))
{
usingConnection(conn);
}
}
catch (Exception ex)
{
if (catchException == null || !catchException(ex))
{
throw;
}
}
}
/*
TODO - re-enable if we port ConnectionStringSecurer
///
/// This method provides the provides a connection string configured with the specified database name.
/// This is also an opportunity to decrypt the connection string based on the encryption/decryption strategy.
/// InvalidConnectionStringException could be thrown since this routine attempts to restore the connection
/// string if 'restoreConnectionString' is true.
///
/// Will only set DatabaseName/ApplicationName if the value is not null.
///
///
public static SqlConnectionStringBuilder ConfigureConnectionString(
string connectionString,
string databaseName,
string applicationName,
bool restoreConnectionString = true)
{
if (restoreConnectionString)
{
// Read the connection string through the persistence layer
connectionString = ConnectionStringSecurer.RestoreConnectionString(
connectionString,
SqlProviderName);
}
SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(connectionString);
builder.Pooling = false;
builder.MultipleActiveResultSets = false;
// Cannot set the applicationName/initialCatalog to null but empty string is valid
if (databaseName != null)
{
builder.InitialCatalog = databaseName;
}
if (applicationName != null)
{
builder.ApplicationName = applicationName;
}
return builder;
}
*/
///
/// Optional 'initializeConnection' routine. This sets the lock and command timeout for the connection.
///
public static void SetLockAndCommandTimeout(IDbConnection conn)
{
ReliableSqlConnection.SetLockAndCommandTimeout(conn);
}
///
/// Opens a IDbConnection, creates a IDbCommand and calls ExecuteNonQuery against the connection.
///
/// The connection string.
/// The scalar T-SQL command.
/// Optional delegate to initialize the IDbCommand before execution.
/// Default is SqlConnectionHelper.SetCommandTimeout
/// delegate called when an exception has occurred. Pass back 'true' to handle the
/// exception, 'false' to throw. If Null is passed in then all exceptions are thrown.
/// Should a retry policy be used when calling ExecuteNonQuery
/// The number of rows affected
public static object ExecuteNonQuery(
SqlConnectionStringBuilder csb,
string commandText,
Action initializeCommand,
Predicate catchException,
bool useRetry)
{
object retObject = null;
OpenConnection(
csb,
(connection) =>
{
retObject = ExecuteNonQuery(connection, commandText, initializeCommand, catchException);
},
catchException,
useRetry);
return retObject;
}
///
/// Creates a IDbCommand and calls ExecuteNonQuery against the connection.
///
/// The connection. This must be opened.
/// The scalar T-SQL command.
/// Optional delegate to initialize the IDbCommand before execution.
/// Default is SqlConnectionHelper.SetCommandTimeout
/// Optional exception handling. Pass back 'true' to handle the
/// exception, 'false' to throw. If Null is passed in then all exceptions are thrown.
/// The number of rows affected
[SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities")]
public static object ExecuteNonQuery(
IDbConnection conn,
string commandText,
Action initializeCommand,
Predicate catchException)
{
Validate.IsNotNull(nameof(conn), conn);
Validate.IsNotNullOrEmptyString(nameof(commandText), commandText);
IDbCommand cmd = null;
try
{
Debug.Assert(conn.State == ConnectionState.Open, "connection passed to ExecuteNonQuery should be open.");
cmd = conn.CreateCommand();
if (initializeCommand == null)
{
initializeCommand = SetCommandTimeout;
}
initializeCommand(cmd);
cmd.CommandText = commandText;
cmd.CommandType = CommandType.Text;
return cmd.ExecuteNonQuery();
}
catch (Exception ex)
{
if (catchException == null || !catchException(ex))
{
throw;
}
}
finally
{
if (cmd != null)
{
cmd.Dispose();
}
}
return null;
}
///
/// Creates a IDbCommand and calls ExecuteScalar against the connection.
///
/// The connection. This must be opened.
/// The scalar T-SQL command.
/// Optional delegate to initialize the IDbCommand before execution.
/// Default is SqlConnectionHelper.SetCommandTimeout
/// Optional exception handling. Pass back 'true' to handle the
/// exception, 'false' to throw. If Null is passed in then all exceptions are thrown.
/// The scalar result
[SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities")]
public static object ExecuteScalar(
IDbConnection conn,
string commandText,
Action initializeCommand = null,
Predicate catchException = null)
{
Validate.IsNotNull(nameof(conn), conn);
Validate.IsNotNullOrEmptyString(nameof(commandText), commandText);
IDbCommand cmd = null;
try
{
Debug.Assert(conn.State == ConnectionState.Open, "connection passed to ExecuteScalar should be open.");
cmd = conn.CreateCommand();
if (initializeCommand == null)
{
initializeCommand = SetCommandTimeout;
}
initializeCommand(cmd);
cmd.CommandText = commandText;
cmd.CommandType = CommandType.Text;
return cmd.ExecuteScalar();
}
catch (Exception ex)
{
if (catchException == null || !catchException(ex))
{
throw;
}
}
finally
{
if (cmd != null)
{
cmd.Dispose();
}
}
return null;
}
///
/// Creates a IDbCommand and calls ExecuteReader against the connection.
///
/// The connection to execute the reader on. This must be opened.
/// The command text to execute
/// A delegate used to read from the reader
/// Optional delegate to initialize the IDbCommand object
/// Optional exception handling. Pass back 'true' to handle the
/// exception, 'false' to throw. If Null is passed in then all exceptions are thrown.
[SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities")]
public static void ExecuteReader(
IDbConnection conn,
string commandText,
Action readResult,
Action initializeCommand = null,
Predicate catchException = null)
{
Validate.IsNotNull(nameof(conn), conn);
Validate.IsNotNullOrEmptyString(nameof(commandText), commandText);
Validate.IsNotNull(nameof(readResult), readResult);
IDbCommand cmd = null;
try
{
Debug.Assert(conn.State == ConnectionState.Open, "connection passed to ExecuteReader should be open.");
cmd = conn.CreateCommand();
if (initializeCommand == null)
{
initializeCommand = SetCommandTimeout;
}
initializeCommand(cmd);
cmd.CommandText = commandText;
cmd.CommandType = CommandType.Text;
using (IDataReader reader = cmd.ExecuteReader())
{
readResult(reader);
}
}
catch (Exception ex)
{
if (catchException == null || !catchException(ex))
{
throw;
}
}
finally
{
if (cmd != null)
{
cmd.Dispose();
}
}
}
///
/// optional 'initializeCommand' routine. This initializes the IDbCommand
///
///
public static void SetCommandTimeout(IDbCommand cmd)
{
Validate.IsNotNull(nameof(cmd), cmd);
cmd.CommandTimeout = CachedServerInfo.GetQueryTimeoutSeconds(cmd.Connection);
}
///
/// Return true if the database is an Azure database
///
///
///
public static bool IsCloud(IDbConnection connection)
{
Validate.IsNotNull(nameof(connection), connection);
if (!(connection.State == ConnectionState.Open))
{
Logger.Write(LogLevel.Warning, Resources.ConnectionPassedToIsCloudShouldBeOpen);
}
Func executeCommand = commandText =>
{
bool result = false;
ExecuteReader(connection,
commandText,
readResult: (reader) =>
{
reader.Read();
int engineEditionId = int.Parse(reader[0].ToString(), CultureInfo.InvariantCulture);
result = IsCloudEngineId(engineEditionId);
}
);
return result;
};
bool isSqlCloud = false;
try
{
isSqlCloud = executeCommand(SqlConnectionHelperScripts.EngineEdition);
}
catch (SqlException)
{
// The default query contains a WITH (NOLOCK). This doesn't work for Azure DW, so when things don't work out,
// we'll fall back to a version without NOLOCK and try again.
isSqlCloud = executeCommand(SqlConnectionHelperScripts.EngineEditionWithLock);
}
return isSqlCloud;
}
private static bool IsCloudEngineId(int engineEditionId)
{
return engineEditionId == SqlAzureEngineEditionId;
}
///
/// Handles the exceptions typically thrown when a SQLConnection is being opened
///
/// True if the exception was handled
public static bool StandardExceptionHandler(Exception ex)
{
Validate.IsNotNull(nameof(ex), ex);
if (ex is SqlException ||
ex is RetryLimitExceededException)
{
return true;
}
if (ex is InvalidCastException ||
ex is ArgumentException || // Thrown when a particular connection string property is invalid (i.e. failover parner = "yes")
ex is InvalidOperationException || // thrown when the connection pool is empty and SQL is down
ex is TimeoutException ||
ex is SecurityException)
{
return true;
}
Logger.Write(LogLevel.Error, ex.ToString());
return false;
}
///
/// Returns the default database path.
///
/// The connection
/// The delegate used to initialize the command
/// The exception handler delegate. If Null is passed in then all exceptions are thrown
public static string GetDefaultDatabaseFilePath(
IDbConnection conn,
Action initializeCommand = null,
Predicate catchException = null)
{
Validate.IsNotNull(nameof(conn), conn);
string filePath = null;
ServerInfo info = GetServerVersion(conn);
if (!info.IsCloud)
{
filePath = GetDefaultDatabasePath(conn, SqlConnectionHelperScripts.GetDatabaseFilePathAndName, initializeCommand, catchException);
}
return filePath;
}
///
/// Returns the log path or null
///
/// The connection
/// The delegate used to initialize the command
/// The exception handler delegate. If Null is passed in then all exceptions are thrown
public static string GetDefaultDatabaseLogPath(
IDbConnection conn,
Action initializeCommand = null,
Predicate catchException = null)
{
Validate.IsNotNull(nameof(conn), conn);
string logPath = null;
ServerInfo info = GetServerVersion(conn);
if (!info.IsCloud)
{
logPath = GetDefaultDatabasePath(conn, SqlConnectionHelperScripts.GetDatabaseLogPathAndName, initializeCommand, catchException);
}
return logPath;
}
///
/// Returns the database path or null
///
/// The connection
/// The command to issue
/// The delegate used to initialize the command
/// The exception handler delegate. If Null is passed in then all exceptions are thrown
private static string GetDefaultDatabasePath(
IDbConnection conn,
string commandText,
Action initializeCommand = null,
Predicate catchException = null)
{
Validate.IsNotNull(nameof(conn), conn);
Validate.IsNotNullOrEmptyString(nameof(commandText), commandText);
string filePath = ExecuteScalar(conn, commandText, initializeCommand, catchException) as string;
if (!String.IsNullOrWhiteSpace(filePath))
{
// Remove filename from the filePath
Uri pathUri;
if (Uri.TryCreate(filePath, UriKind.Absolute, out pathUri) == false)
{
// Invalid Uri
return null;
}
// Get a current directory path relative to the pathUri
// This will remove filename from the uri.
Uri filePathUri = new Uri(pathUri, ".");
// For file uri we need to get LocalPath instead of file:// url
filePath = filePathUri.IsFile ? filePathUri.LocalPath : filePathUri.OriginalString;
}
return filePath;
}
///
/// Returns true if the database is readonly. This routine will swallow the exceptions you might expect from SQL using StandardExceptionHandler.
///
public static bool IsDatabaseReadonly(SqlConnectionStringBuilder builder)
{
Validate.IsNotNull(nameof(builder), builder);
if (builder == null)
{
throw new ArgumentNullException("builder");
}
bool isDatabaseReadOnly = false;
OpenConnection(
builder,
(connection) =>
{
string commandText = String.Format(CultureInfo.InvariantCulture, SqlConnectionHelperScripts.CheckDatabaseReadonly, builder.InitialCatalog);
ExecuteReader(connection,
commandText,
readResult: (reader) =>
{
if (reader.Read())
{
string currentSetting = reader.GetString(1);
if (String.Compare(currentSetting, "ON", StringComparison.OrdinalIgnoreCase) == 0)
{
isDatabaseReadOnly = true;
}
}
});
},
(ex) =>
{
Logger.Write(LogLevel.Error, ex.ToString());
return StandardExceptionHandler(ex); // handled
},
useRetry: true);
return isDatabaseReadOnly;
}
public class ServerInfo
{
public int ServerMajorVersion;
public int ServerMinorVersion;
public int ServerReleaseVersion;
public int EngineEditionId;
public string ServerVersion;
public string ServerLevel;
public string ServerEdition;
public bool IsCloud;
public int AzureVersion;
// In SQL 2012 SP1 Selective XML indexes were added. There is bug where upgraded databases from previous versions
// of SQL Server do not have their metadata upgraded to include the xml_index_type column in the sys.xml_indexes view. Because
// of this, we must detect the presence of the column to determine if we can query for Selective Xml Indexes
public bool IsSelectiveXmlIndexMetadataPresent;
public bool IsAzureV1
{
get
{
return IsCloud && AzureVersion == 1;
}
}
public string OsVersion;
}
public static bool TryGetServerVersion(string connectionString, out ServerInfo serverInfo)
{
serverInfo = null;
SqlConnectionStringBuilder builder;
if (!TryGetConnectionStringBuilder(connectionString, out builder))
{
return false;
}
serverInfo = GetServerVersion(builder);
return true;
}
///
/// Returns the version of the server. This routine will throw if an exception is encountered.
///
public static ServerInfo GetServerVersion(SqlConnectionStringBuilder csb)
{
Validate.IsNotNull(nameof(csb), csb);
ServerInfo serverInfo = null;
OpenConnection(
csb,
(connection) =>
{
serverInfo = GetServerVersion(connection);
},
catchException: null, // Always throw
useRetry: true);
return serverInfo;
}
///
/// Returns the version of the server. This routine will throw if an exception is encountered.
///
public static ServerInfo GetServerVersion(IDbConnection connection)
{
Validate.IsNotNull(nameof(connection), connection);
if (!(connection.State == ConnectionState.Open))
{
Logger.Write(LogLevel.Error, "connection passed to GetServerVersion should be open.");
}
Func getServerInfo = commandText =>
{
ServerInfo serverInfo = new ServerInfo();
ExecuteReader(
connection,
commandText,
delegate (IDataReader reader)
{
reader.Read();
int engineEditionId = Int32.Parse(reader[0].ToString(), CultureInfo.InvariantCulture);
serverInfo.EngineEditionId = engineEditionId;
serverInfo.IsCloud = IsCloudEngineId(engineEditionId);
serverInfo.ServerVersion = reader[1].ToString();
serverInfo.ServerLevel = reader[2].ToString();
serverInfo.ServerEdition = reader[3].ToString();
if (reader.FieldCount > 4)
{
// Detect the presence of SXI
serverInfo.IsSelectiveXmlIndexMetadataPresent = reader.GetInt32(4) == 1;
}
// The 'ProductVersion' server property is of the form ##.#[#].####.#,
Version serverVersion = new Version(serverInfo.ServerVersion);
// The server version is of the form ##.##.####,
serverInfo.ServerMajorVersion = serverVersion.Major;
serverInfo.ServerMinorVersion = serverVersion.Minor;
serverInfo.ServerReleaseVersion = serverVersion.Build;
if (serverInfo.IsCloud)
{
serverInfo.AzureVersion = serverVersion.Major > 11 ? 2 : 1;
}
try
{
CachedServerInfo.AddOrUpdateIsAzure(connection, serverInfo.IsCloud);
}
catch (Exception ex)
{
//we don't want to fail the normal flow if any unexpected thing happens
//during caching although it's unlikely. So we just log the exception and ignore it
Logger.Write(LogLevel.Error, Resources.FailedToCacheIsCloud);
Logger.Write(LogLevel.Error, ex.ToString());
}
});
// Also get the OS Version
ExecuteReader(
connection,
SqlConnectionHelperScripts.GetOsVersion,
delegate (IDataReader reader)
{
reader.Read();
serverInfo.OsVersion = reader[0].ToString();
});
return serverInfo;
};
ServerInfo result = null;
try
{
result = getServerInfo(SqlConnectionHelperScripts.EngineEdition);
}
catch (SqlException)
{
// The default query contains a WITH (NOLOCK). This doesn't work for Azure DW, so when things don't work out,
// we'll fall back to a version without NOLOCK and try again.
result = getServerInfo(SqlConnectionHelperScripts.EngineEditionWithLock);
}
return result;
}
public static string GetServerName(IDbConnection connection)
{
return new DbConnectionWrapper(connection).DataSource;
}
public static string ReadServerVersion(IDbConnection connection)
{
return new DbConnectionWrapper(connection).ServerVersion;
}
///
/// Converts to a SqlConnection by casting (if we know it is actually a SqlConnection)
/// or by getting the underlying connection (if it's a ReliableSqlConnection)
///
public static SqlConnection GetAsSqlConnection(IDbConnection connection)
{
return new DbConnectionWrapper(connection).GetAsSqlConnection();
}
/* TODO - CloneAndOpenConnection() requires IClonable, which doesn't exist in .NET Core
///
/// Clones a connection and ensures it's opened.
/// If it's a SqlConnection it will clone it,
/// and for ReliableSqlConnection it will clone the underling connection.
/// The reason the entire ReliableSqlConnection is not cloned is that it includes
/// several callbacks and we don't want to try and handle deciding how to clone these
/// yet.
///
public static SqlConnection CloneAndOpenConnection(IDbConnection connection)
{
return new DbConnectionWrapper(connection).CloneAndOpenConnection();
}
*/
public class ServerAndDatabaseInfo : ServerInfo
{
public int DbCompatibilityLevel;
public string DatabaseName;
}
internal static bool TryGetConnectionStringBuilder(string connectionString, out SqlConnectionStringBuilder builder)
{
builder = null;
if (String.IsNullOrEmpty(connectionString))
{
// Connection string is not valid
return false;
}
// Attempt to initialize the builder
Exception handledEx = null;
try
{
builder = new SqlConnectionStringBuilder(connectionString);
}
catch (KeyNotFoundException ex)
{
handledEx = ex;
}
catch (FormatException ex)
{
handledEx = ex;
}
catch (ArgumentException ex)
{
handledEx = ex;
}
if (handledEx != null)
{
Logger.Write(LogLevel.Error, String.Format(Resources.ErrorParsingConnectionString, handledEx));
return false;
}
return true;
}
/*
///
/// Get the version of the server and database using
/// the connection string provided. This routine will
/// throw if an exception is encountered.
///
/// The connection string used to connect to the database.
/// Basic information about the server
public static bool GetServerAndDatabaseVersion(string connectionString, out ServerAndDatabaseInfo info)
{
bool foundVersion = false;
info = new ServerAndDatabaseInfo { IsCloud = false, ServerMajorVersion = -1, DbCompatibilityLevel = -1, DatabaseName = String.Empty };
SqlConnectionStringBuilder builder;
if (!TryGetConnectionStringBuilder(connectionString, out builder))
{
return false;
}
// The database name is either the InitialCatalog or the AttachDBFilename. The
// AttachDBFilename is used if an mdf file is specified in the connections dialog.
if (String.IsNullOrEmpty(builder.InitialCatalog) ||
String.IsNullOrEmpty(builder.AttachDBFilename))
{
builder.Pooling = false;
string tempDatabaseName = String.Empty;
int tempDbCompatibilityLevel = 0;
ServerInfo serverInfo = null;
OpenConnection(
builder,
(connection) =>
{
// Set the lock timeout to 3 seconds
SetLockAndCommandTimeout(connection);
serverInfo = GetServerVersion(connection);
tempDatabaseName = (String.IsNullOrEmpty(builder.InitialCatalog) == false) ?
builder.InitialCatalog : builder.AttachDBFilename;
// If at this point the dbName remained an empty string then
// we should get the database name from the open IDbConnection
if (String.IsNullOrEmpty(tempDatabaseName))
{
tempDatabaseName = connection.Database;
}
// SQL Azure does not support custom DBCompat values.
if (!serverInfo.IsAzureV1)
{
SqlParameter databaseNameParameter = new SqlParameter(
"@dbname",
SqlDbType.NChar,
128,
ParameterDirection.Input,
false,
0,
0,
null,
DataRowVersion.Default,
tempDatabaseName);
object compatibilityLevel;
using (IDbCommand versionCommand = connection.CreateCommand())
{
versionCommand.CommandText = "SELECT compatibility_level FROM sys.databases WITH (NOLOCK) WHERE name = @dbname";
versionCommand.CommandType = CommandType.Text;
versionCommand.Parameters.Add(databaseNameParameter);
compatibilityLevel = versionCommand.ExecuteScalar();
}
// value is null if db is not online
foundVersion = compatibilityLevel != null && !(compatibilityLevel is DBNull);
if(foundVersion)
{
tempDbCompatibilityLevel = (byte)compatibilityLevel;
}
else
{
string conString = connection.ConnectionString == null ? "null" : connection.ConnectionString;
string dbName = tempDatabaseName == null ? "null" : tempDatabaseName;
string message = string.Format(CultureInfo.CurrentCulture,
"Querying database compatibility level failed. Connection string: '{0}'. dbname: '{1}'.",
conString, dbName);
Tracer.TraceEvent(TraceEventType.Error, TraceId.CoreServices, message);
}
}
else
{
foundVersion = true;
}
},
catchException: null, // Always throw
useRetry: true);
info.IsCloud = serverInfo.IsCloud;
info.ServerMajorVersion = serverInfo.ServerMajorVersion;
info.ServerMinorVersion = serverInfo.ServerMinorVersion;
info.ServerReleaseVersion = serverInfo.ServerReleaseVersion;
info.ServerVersion = serverInfo.ServerVersion;
info.ServerLevel = serverInfo.ServerLevel;
info.ServerEdition = serverInfo.ServerEdition;
info.AzureVersion = serverInfo.AzureVersion;
info.DatabaseName = tempDatabaseName;
info.DbCompatibilityLevel = tempDbCompatibilityLevel;
}
return foundVersion;
}
*/
///
/// Returns true if the authenticating database is master, otherwise false. An example of
/// false is when the user is a contained user connecting to a contained database.
///
public static bool IsAuthenticatingDatabaseMaster(IDbConnection connection)
{
try
{
const string sqlCommand =
@"use [{0}];
if (db_id() = 1)
begin
-- contained auth is 0 when connected to master
select 0
end
else
begin
-- need dynamic sql so that we compile this query only when we know resource db is available
exec('select case when authenticating_database_id = 1 then 0 else 1 end from sys.dm_exec_sessions where session_id = @@SPID')
end";
string finalCmd = null;
if (!String.IsNullOrWhiteSpace(connection.Database))
{
finalCmd = String.Format(CultureInfo.InvariantCulture, sqlCommand, connection.Database);
}
else
{
finalCmd = String.Format(CultureInfo.InvariantCulture, sqlCommand, "master");
}
object retValue = ExecuteScalar(connection, finalCmd);
if (retValue != null && retValue.ToString() == "1")
{
// contained auth is 0 when connected to non-master
return false;
}
return true;
}
catch (Exception ex)
{
if (StandardExceptionHandler(ex))
{
return true;
}
throw;
}
}
///
/// Returns true if the authenticating database is master, otherwise false. An example of
/// false is when the user is a contained user connecting to a contained database.
///
public static bool IsAuthenticatingDatabaseMaster(SqlConnectionStringBuilder builder)
{
bool authIsMaster = true;
OpenConnection(
builder,
usingConnection: (connection) =>
{
authIsMaster = IsAuthenticatingDatabaseMaster(connection);
},
catchException: StandardExceptionHandler, // Don't throw unless it's an unexpected exception
useRetry: true);
return authIsMaster;
}
///
/// Returns the form of the server as a it's name - replaces . and (localhost)
///
public static string GetCompleteServerName(string server)
{
if (String.IsNullOrEmpty(server))
{
return server;
}
int nlen = 0;
if (server[0] == '.')
{
nlen = 1;
}
else if (String.Compare(server, Constants.Local, StringComparison.OrdinalIgnoreCase) == 0)
{
nlen = Constants.Local.Length;
}
else if (String.Compare(server, 0, ServerNameLocalhost, 0, ServerNameLocalhost.Length, StringComparison.OrdinalIgnoreCase) == 0)
{
nlen = ServerNameLocalhost.Length;
}
if (nlen > 0)
{
string strMachine = Environment.MachineName;
if (server.Length == nlen)
return strMachine;
if (server.Length > (nlen + 1) && server[nlen] == '\\') // instance
{
string strRet = strMachine + server.Substring(nlen);
return strRet;
}
}
return server;
}
/*
///
/// Processes a user-supplied connection string and provides a trimmed connection string
/// that eliminates everything except for DataSource, InitialCatalog, UserId, Password,
/// ConnectTimeout, Encrypt, TrustServerCertificate and IntegratedSecurity.
///
/// When connection string is invalid
public static string TrimConnectionString(string connectionString)
{
Exception handledException;
try
{
SqlConnectionStringBuilder scsb = new SqlConnectionStringBuilder(connectionString);
return TrimConnectionStringBuilder(scsb).ConnectionString;
}
catch (ArgumentException exception)
{
handledException = exception;
}
catch (KeyNotFoundException exception)
{
handledException = exception;
}
catch (FormatException exception)
{
handledException = exception;
}
throw new InvalidConnectionStringException(handledException);
}
*/
///
/// Sql 2012 PCU1 introduces breaking changes to metadata queries and adds new Selective XML Index support.
/// This method allows components to detect if the represents a build of SQL 2012 after RTM.
///
public static bool IsVersionGreaterThan2012RTM(ServerInfo _serverInfo)
{
return _serverInfo.ServerMajorVersion > 11 ||
// Use the presence of SXI metadata rather than build number as upgrade bugs leave out the SXI metadata for some upgraded databases.
_serverInfo.ServerMajorVersion == 11 && _serverInfo.IsSelectiveXmlIndexMetadataPresent;
}
// SQL Server: Defect 1122301: ReliableConnectionHelper does not maintain ApplicationIntent
// The ApplicationIntent and MultiSubnetFailover property is not introduced to .NET until .NET 4.0 update 2
// However, DacFx is officially depends on .NET 4.0 RTM
// So here we want to support both senarios, on machine with 4.0 RTM installed, it will ignore these 2 properties
// On machine with higher .NET version which included those properties, it will pick them up.
public static void TryAddAlwaysOnConnectionProperties(SqlConnectionStringBuilder userBuilder, SqlConnectionStringBuilder trimBuilder)
{
if (userBuilder.ContainsKey(ApplicationIntent))
{
trimBuilder[ApplicationIntent] = userBuilder[ApplicationIntent];
}
if (userBuilder.ContainsKey(MultiSubnetFailover))
{
trimBuilder[MultiSubnetFailover] = userBuilder[MultiSubnetFailover];
}
}
/* TODO - this relies on porting SqlAuthenticationMethodUtils
///
/// Processes a user-supplied connection string and provides a trimmed connection string
/// that eliminates everything except for DataSource, InitialCatalog, UserId, Password,
/// ConnectTimeout, Encrypt, TrustServerCertificate, IntegratedSecurity and Pooling.
///
///
/// Pooling is always set to false to avoid connections remaining open.
///
/// When connection string is invalid
public static SqlConnectionStringBuilder TrimConnectionStringBuilder(SqlConnectionStringBuilder userBuilder, Action throwException = null)
{
Exception handledException;
if (throwException == null)
{
throwException = (propertyName) =>
{
throw new InvalidConnectionStringException(String.Format(CultureInfo.CurrentCulture, Resources.UnsupportedConnectionStringArgument, propertyName));
};
}
if (!String.IsNullOrEmpty(userBuilder.AttachDBFilename))
{
throwException("AttachDBFilename");
}
if (userBuilder.UserInstance)
{
throwException("User Instance");
}
try
{
SqlConnectionStringBuilder trimBuilder = new SqlConnectionStringBuilder();
if (String.IsNullOrWhiteSpace(userBuilder.DataSource))
{
throw new InvalidConnectionStringException();
}
trimBuilder.ConnectTimeout = userBuilder.ConnectTimeout;
trimBuilder.DataSource = userBuilder.DataSource;
if (false == String.IsNullOrWhiteSpace(userBuilder.InitialCatalog))
{
trimBuilder.InitialCatalog = userBuilder.InitialCatalog;
}
trimBuilder.IntegratedSecurity = userBuilder.IntegratedSecurity;
if (!String.IsNullOrWhiteSpace(userBuilder.UserID))
{
trimBuilder.UserID = userBuilder.UserID;
}
if (!String.IsNullOrWhiteSpace(userBuilder.Password))
{
trimBuilder.Password = userBuilder.Password;
}
trimBuilder.TrustServerCertificate = userBuilder.TrustServerCertificate;
trimBuilder.Encrypt = userBuilder.Encrypt;
if (String.IsNullOrWhiteSpace(userBuilder.ApplicationName) ||
String.Equals(BuilderWithDefaultApplicationName.ApplicationName, userBuilder.ApplicationName, StringComparison.Ordinal))
{
trimBuilder.ApplicationName = DacFxApplicationName;
}
else
{
trimBuilder.ApplicationName = userBuilder.ApplicationName;
}
TryAddAlwaysOnConnectionProperties(userBuilder, trimBuilder);
if (SqlAuthenticationMethodUtils.IsAuthenticationSupported())
{
SqlAuthenticationMethodUtils.SetAuthentication(userBuilder, trimBuilder);
}
if (SqlAuthenticationMethodUtils.IsCertificateSupported())
{
SqlAuthenticationMethodUtils.SetCertificate(userBuilder, trimBuilder);
}
trimBuilder.Pooling = false;
return trimBuilder;
}
catch (ArgumentException exception)
{
handledException = exception;
}
catch (KeyNotFoundException exception)
{
handledException = exception;
}
catch (FormatException exception)
{
handledException = exception;
}
throw new InvalidConnectionStringException(handledException);
}
public static bool TryCreateConnectionStringBuilder(string connectionString, out SqlConnectionStringBuilder builder, out Exception handledException)
{
bool success = false;
builder = null;
handledException = null;
try
{
builder = TrimConnectionStringBuilder(new SqlConnectionStringBuilder(connectionString));
success = true;
}
catch (InvalidConnectionStringException e)
{
handledException = e;
}
catch (ArgumentException exception)
{
handledException = exception;
}
catch (KeyNotFoundException exception)
{
handledException = exception;
}
catch (FormatException exception)
{
handledException = exception;
}
finally
{
if (handledException != null)
{
success = false;
}
}
return success;
}
*/
}
}