Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions FAnsi.Core/Connections/IManagedConnection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;
using System.Data.Common;
using FAnsi.Discovery;

namespace FAnsi.Connections;

/// <summary>
/// Wrapper for DbConnection and optional DbTransaction.
/// </summary>
public interface IManagedConnection : IDisposable
{
/// <summary>
/// DbConnection being wrapped
/// </summary>
DbConnection Connection { get; }

/// <summary>
/// Optional - DbTransaction being wrapped if one has been started or null
/// </summary>
DbTransaction? Transaction { get; }

/// <summary>
/// Optional - transaction being run (See <see cref="DiscoveredServer.BeginNewTransactedConnection"/>. If this is not null then <see cref="Transaction"/> should also be not null.
/// </summary>
IManagedTransaction? ManagedTransaction { get; }

/// <summary>
/// True to close the connection in the Dispose step. If <see cref="IManagedConnection"/> opened the connection itself during construction then this flag will default
/// to true otherwise it will default to false.
/// </summary>
bool CloseOnDispose { get; set; }

/// <summary>
/// Creates a new shallow copy instance of the <see cref="IManagedConnection"/>. This will point to the same
/// underlying <see cref="DbConnection"/> and <see cref="DbTransaction"/> (if any).
/// </summary>
/// <returns></returns>
ManagedConnection Clone();
}
30 changes: 30 additions & 0 deletions FAnsi.Core/Connections/IManagedTransaction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Data.Common;

namespace FAnsi.Connections;

/// <summary>
/// Wrapper for DbTransaction that associates it with a specific DbConnection. Helps simplify calls to information
/// methods such as DiscoveredTable.GetRowCount etc during the middle of an ongoing database transaction
/// </summary>
public interface IManagedTransaction
{
/// <summary>
/// The DbConnection that the <see cref="Transaction"/> is running on
/// </summary>
DbConnection Connection { get; }

/// <summary>
/// The DbTransaction being wrapped
/// </summary>
DbTransaction Transaction { get; }

/// <summary>
/// Calls <see cref="DbTransaction.Rollback()"/> and closes/disposes the <see cref="Connection"/>
/// </summary>
void AbandonAndCloseConnection();

/// <summary>
/// Calls <see cref="DbTransaction.Commit"/> and closes/disposes the <see cref="Connection"/>
/// </summary>
void CommitAndCloseConnection();
}
50 changes: 50 additions & 0 deletions FAnsi.Core/Connections/ManagedConnection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System.Data;
using System.Data.Common;
using System.Diagnostics;
using FAnsi.Discovery;

namespace FAnsi.Connections;

/// <inheritdoc/>
public sealed class ManagedConnection : IManagedConnection
{
/// <inheritdoc/>
public DbConnection Connection { get; }

/// <inheritdoc/>
public DbTransaction? Transaction { get; }

/// <inheritdoc/>
public IManagedTransaction? ManagedTransaction { get; }

/// <inheritdoc/>
public bool CloseOnDispose { get; set; }

internal ManagedConnection(DiscoveredServer discoveredServer, IManagedTransaction? managedTransaction)
{
//get a new connection or use the existing one within the transaction
Connection = discoveredServer.GetConnection(managedTransaction);

//if there is a transaction, also store the transaction
ManagedTransaction = managedTransaction;
Transaction = managedTransaction?.Transaction;

//if there isn't a transaction then we opened a new connection, so we had better remember to close it again
if (managedTransaction != null) return;

CloseOnDispose = true;
Debug.Assert(Connection.State == ConnectionState.Closed);
Connection.Open();
}

public ManagedConnection Clone() => (ManagedConnection)MemberwiseClone();

/// <summary>
/// Closes and disposes the DbConnection unless this class is part of an <see cref="IManagedTransaction"/>
/// </summary>
public void Dispose()
{
if (CloseOnDispose)
Connection.Dispose();
}
}
69 changes: 69 additions & 0 deletions FAnsi.Core/Connections/ManagedTransaction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System;
using System.Data.Common;
using System.Diagnostics;

namespace FAnsi.Connections;

/// <inheritdoc/>
public sealed class ManagedTransaction : IManagedTransaction
{
/// <inheritdoc/>
public DbConnection Connection { get; }

/// <inheritdoc/>
public DbTransaction Transaction { get; }

internal ManagedTransaction(DbConnection connection, DbTransaction transaction)
{
Connection = connection;
Transaction = transaction;
}

private bool _closed;

/// <summary>
/// Attempts to rollback the DbTransaction (swallowing any Exception) and closes/disposes the DbConnection
/// </summary>
public void AbandonAndCloseConnection()
{
if(_closed)
return;

_closed = true;

try
{
Transaction.Rollback();
}
catch (Exception e)
{
Trace.WriteLine($"Transaction rollback failed during AbandonAndCloseConnection{e.Message}");
}
finally
{
Connection.Close();
Connection.Dispose();
}
}

/// <summary>
/// Attempts to commit the DbTransaction and then closes/disposes the DbConnection
/// </summary>
public void CommitAndCloseConnection()
{
if(_closed)
return;

_closed = true;

try
{
Transaction.Commit();
}
finally
{
Connection.Close();
Connection.Dispose();
}
}
}
150 changes: 150 additions & 0 deletions FAnsi.Core/DatabaseOperationArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
using System;
using System.Data;
using System.Data.Common;
using System.Threading;
using System.Threading.Tasks;
using FAnsi.Connections;
using FAnsi.Discovery;

namespace FAnsi;

/// <summary>
/// Arguments for facilitating long running sql operations which the user/system might want to cancel mid way through.
/// </summary>
public sealed class DatabaseOperationArgs
{
/// <summary>
/// If using an ongoing connection/transaction. Otherwise null.
/// </summary>
public IManagedTransaction? TransactionIfAny { get; set; }

/// <summary>
/// Time to allow <see cref="DbCommand"/> to run before cancelling (this is db timeout and doesn't affect <see cref="CancellationToken"/>)
/// </summary>
public int TimeoutInSeconds { get; set; }

/// <summary>
/// Optional, if provided all commands interacting with these args should cancel if the command was cancelled
/// </summary>
public CancellationToken CancellationToken;

public DatabaseOperationArgs()
{

}
public DatabaseOperationArgs(IManagedTransaction transactionIfAny, int timeoutInSeconds,
CancellationToken cancellationToken)
{
TransactionIfAny = transactionIfAny;
CancellationToken = cancellationToken;
TimeoutInSeconds = timeoutInSeconds;
}

/// <summary>
/// Sets the timeout and cancellation on <paramref name="cmd"/> then runs <see cref="DbCommand.ExecuteNonQueryAsync()"/> with the
/// <see cref="CancellationToken"/> (if any) and blocks till the call completes.
///
/// </summary>
/// <param name="cmd"></param>
/// <exception cref="OperationCanceledException"></exception>
public int ExecuteNonQuery(DbCommand cmd)
{
return Execute(cmd, ()=>cmd.ExecuteNonQueryAsync(CancellationToken));
}
/// <summary>
/// Sets the timeout and cancellation on <paramref name="cmd"/> then runs <see cref="DbCommand.ExecuteScalar()"/> with the
/// <see cref="CancellationToken"/> (if any) and blocks till the call completes.
///
/// </summary>
/// <param name="cmd"></param>
/// <exception cref="OperationCanceledException"></exception>
public object? ExecuteScalar(DbCommand cmd)
{
return Execute(cmd, ()=>cmd.ExecuteScalarAsync(CancellationToken));
}

private T Execute<T>(DbCommand cmd, Func<Task<T>> method)
{
Hydrate(cmd);
var t = method();

try
{
switch (t.Status)
{
case TaskStatus.Faulted:
throw t.Exception?? new Exception("Task crashed without Exception!");
case TaskStatus.Canceled:
throw new OperationCanceledException();
default:
t.Wait();
break;
}
}
catch (AggregateException e)
{
if (e.InnerExceptions.Count == 1)
throw e.InnerExceptions[0];

throw;
}

if (!t.IsCompleted)
cmd.Cancel();

if (t.Exception == null) return t.Result;

if (t.Exception.InnerExceptions.Count == 1)
throw t.Exception.InnerExceptions[0];

throw t.Exception;
}

public void Fill(DbDataAdapter da, DbCommand cmd, DataTable dt)
{
Hydrate(cmd);

CancellationToken.ThrowIfCancellationRequested();

if(CancellationToken.CanBeCanceled)
dt.RowChanged += ThrowIfCancelled;

da.Fill(dt);
CancellationToken.ThrowIfCancellationRequested();
}

private void ThrowIfCancelled(object sender, DataRowChangeEventArgs e)
{
CancellationToken.ThrowIfCancellationRequested();
}

private void Hydrate(DbCommand cmd)
{
cmd.CommandTimeout = TimeoutInSeconds;
CancellationToken.ThrowIfCancellationRequested();
}

/// <summary>
/// Opens a new connection or passes back an existing opened connection (that matches
/// <see cref="TransactionIfAny"/>). This command should be wrapped in a using statement
/// </summary>
/// <param name="table"></param>
/// <returns></returns>
public IManagedConnection GetManagedConnection(DiscoveredTable table) => GetManagedConnection(table.Database.Server);

/// <summary>
/// Opens a new connection or passes back an existing opened connection (that matches
/// <see cref="TransactionIfAny"/>). This command should be wrapped in a using statement
/// </summary>
/// <param name="database"></param>
/// <returns></returns>
public IManagedConnection GetManagedConnection(DiscoveredDatabase database) => GetManagedConnection(database.Server);

/// <summary>
/// Opens a new connection or passes back an existing opened connection (that matches
/// <see cref="TransactionIfAny"/>). This command should be wrapped in a using statement
/// </summary>
/// <param name="server"></param>
/// <returns></returns>
public IManagedConnection GetManagedConnection(DiscoveredServer server) => server.GetManagedConnection(TransactionIfAny);
}
27 changes: 27 additions & 0 deletions FAnsi.Core/DatabaseType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace FAnsi;

/// <summary>
/// Describes a specific DBMS implementation you are talking to
/// </summary>
public enum DatabaseType
{
/// <summary>
/// Any Microsoft Sql Server database (e.g. Express etc). Does not include Access.
/// </summary>
MicrosoftSQLServer,

/// <summary>
/// My Sql database engine.
/// </summary>
MySql,

/// <summary>
/// Oracle database engine
/// </summary>
Oracle,

/// <summary>
/// PostgreSql database engine
/// </summary>
PostgreSql
}
Loading
Loading