#region License // Copyright 2005-2019 Paul Kohler (https://github.com/paulkohler/minisqlquery). All rights reserved. // This source code is made available under the terms of the GNU Lesser General Public License v3.0 // https://github.com/paulkohler/minisqlquery/blob/master/LICENSE #endregion using System; using System.Data; using System.Data.Common; using System.Data.SqlClient; using System.Diagnostics; namespace MiniSqlQuery.Core { /// /// Helper class to encapsulate query execution /// public class QueryRunner { /// /// The connection string value. /// private readonly string _connectionString; /// /// The enable query batching value. /// private readonly bool _enableQueryBatching; private readonly int _commandTimeout; /// /// The provider factory. /// private readonly DbProviderFactory _factory; private DbCommand _command; /// /// Initializes a new instance of the class. /// /// The factory. /// The connection string. /// The enable query batching. /// public QueryRunner(DbProviderFactory factory, string connectionString, bool enableQueryBatching, int commandTimeout) { _factory = factory; _connectionString = connectionString; _enableQueryBatching = enableQueryBatching; _commandTimeout = commandTimeout; Messages = string.Empty; } /// /// The batch progress. /// public event EventHandler BatchProgress; /// /// Gets or sets the for this query. /// /// The query batch. public QueryBatch Batch { get; protected set; } /// /// Gets or sets Exception if any. /// /// The exception. public DbException Exception { get; protected set; } /// /// Gets or sets a value indicating whether the query runner is busy. /// /// The is busy value. public bool IsBusy { get; set; } /// /// Gets or sets the messages if any. /// /// The messages. public string Messages { get; protected set; } /// /// Creates an instance of a query runner for the specified database. /// /// The factory. /// The connection string. /// The enable query batching. /// The command timeout. /// /// A instance acording to the parameters. /// /// /// /// var runner = QueryRunner.Create(DbProviderFactories.GetFactory("System.Data.SqlClient"), connStr, true); /// runner.ExecuteQuery("select * from Employees\r\nGO\r\nSelect * from Products"); /// // runner.Batch.Queries.Count == 2 // /// /// public static QueryRunner Create(DbProviderFactory factory, string connectionString, bool enableQueryBatching, int commandTimeout) { if (factory.GetType().Name == "SqlClientFactory") { return new SqlQueryRunner(factory, connectionString, enableQueryBatching, commandTimeout); } // otherwise ise the default return new QueryRunner(factory, connectionString, enableQueryBatching, commandTimeout); } /// /// Tests the database connection using the specified provider. /// /// Name of the provider. /// The connection string. /// If the connection was successful, null; otherwise the exception object. public static Exception TestDbConnection(string providerName, string connectionString) { try { DbProviderFactory factory = DbProviderFactories.GetFactory(providerName); using (DbConnection connection = factory.CreateConnection()) { connection.ConnectionString = connectionString; connection.Open(); if (connection.State == ConnectionState.Open) { return null; } } } catch (Exception e) { return e; } throw new InvalidOperationException("Connection test failed"); } /// /// Executes the query. /// /// The SQL to execute. public void ExecuteQuery(string sql) { ValidateState(); DbConnection dbConnection = null; DbDataAdapter adapter = null; _command = null; Query query; // In the case of connection errors the error messages were getting lost, provide a default batch object: Batch = new QueryBatch(sql); try { IsBusy = true; dbConnection = _factory.CreateConnection(); dbConnection.ConnectionString = _connectionString; dbConnection.Open(); Messages = string.Empty; SubscribeToMessages(dbConnection); if (_enableQueryBatching) { Batch = QueryBatch.Parse(sql); } else { Batch = new QueryBatch(sql); } Batch.StartTime = DateTime.Now; adapter = _factory.CreateDataAdapter(); _command = dbConnection.CreateCommand(); _command.CommandType = CommandType.Text; SetCommandTimeout(_command, _commandTimeout); adapter.SelectCommand = _command; int queryCount = Batch.Queries.Count; for (int i = 0; i < queryCount; i++) { query = Batch.Queries[i]; _command.CommandText = query.Sql; query.Result = new DataSet("Batch " + (i + 1)); query.StartTime = DateTime.Now; adapter.Fill(query.Result); query.EndTime = DateTime.Now; OnBatchProgress(new BatchProgressEventArgs(query, queryCount, i + 1)); } } catch (DbException dbException) { HandleBatchException(dbException); } finally { if (Batch != null) { Batch.EndTime = DateTime.Now; } if (adapter != null) { adapter.Dispose(); } if (_command != null) { _command.Dispose(); } IsBusy = false; UnsubscribeFromMessages(dbConnection); } if (Batch != null) { Batch.Messages = Messages; } } /// /// Cancel the executing command (if busy). /// /// /// Note that this relies on the implementation of the DbCommand.Cancel operation. /// public void Cancel() { if (IsBusy && _command != null) { _command.Cancel(); } } /// /// Sets the command timeout, currently only tested against MSSQL. /// /// The command. /// The command timeout. private void SetCommandTimeout(IDbCommand cmd, int commandTimeout) { if (_factory is SqlClientFactory) { if (cmd == null) { throw new ArgumentNullException("cmd"); } cmd.CommandTimeout = commandTimeout; } else { Trace.WriteLine("Command Timeout only supported by SQL Client (so far)"); } } /// /// The handle batch exception. /// /// The db exception. protected virtual void HandleBatchException(DbException dbException) { Exception = dbException; Messages += dbException.Message + Environment.NewLine; } /// /// The on batch progress. /// /// The events. protected void OnBatchProgress(BatchProgressEventArgs e) { EventHandler progress = BatchProgress; if (progress != null) { progress(this, e); } } /// /// The subscribe to messages. /// /// The connection. protected virtual void SubscribeToMessages(DbConnection connection) { } /// /// The unsubscribe from messages. /// /// The connection. protected virtual void UnsubscribeFromMessages(DbConnection connection) { } /// /// Ensures that there is enough information available to the class to execute a query. /// /// If there is no connection, "Supply a connection." /// If there is no connection, "Supply a provider." private void ValidateState() { if (string.IsNullOrEmpty(_connectionString)) { throw new InvalidOperationException("Supply a connection."); } if (_factory == null) { throw new InvalidOperationException("Supply a provider."); } } } }