-1

In my app, I use an class (MySqlClass) to execute and download data from my database.

public class MySqlClass
{
    public void ExecuteQuery(string query) { /* ... */ }

    public DataSet GetDataSet(string query) { /* ... */ }

    public void Transaction(Action queryToCommit, Action whenRollback) { /* ... */ }
}

For example :

public class MyApp
{
    List<MyObjectClass> myList = MyObjectClass.GetMyObjectClass("white");
}

public class MyObjectClass
{
    private static MySqlClass sqlConn = new MySqlClass();

    public static List<MyObjectClass> GetMyObjectClass(string color)
    {
        List<MyObjectClass> obj = new List<MyObjectClass>();

        using (DataSet ds = sqlConn.GetDataSet(" ... my query ... "))
        {
            foreach (DataRow dr in ds.Tables[0].Rows)
            {
                obj.Add(ConvertDataRow(dr));
            }
        }

        return obj;
    }

    public static MyObjectClass ConvertDataRow(DataRow dr) { /* ... */ }
}

For some criticals utilizations, I want use transaction WITHOUT modify originals functions. I developed for this "Transaction" function in MySqlClass :

public void Transaction(Action queryToCommit, Action whenRollback)
{
    CancellationTokenSource cts = new CancellationTokenSource();
    try
    {
        Task executeAction = new Task(new Action(() =>
            {
                queryToCommit.Invoke(); // user code executed here
                cts.Token.ThrowIfCancellationRequested();
            }),
            cts.Token
            );

        executeAction.Start();
        executeAction.Wait(cts.Token);

        Commit();
    }
    catch (Exception)
    {
        Rollback();

        whenRollback.Invoke();
        /* ... */
    }
}

In my app :

public class MyApp
{
    MySqlClass sqlConn = new MySqlClass();
    List<MyObjectClass> myList = MyObjectClass.GetMyObjectClass("white");

    private void WithoutTransaction()
    {
        MyObjectClass objA = new MyObjectClass();
        MyObjectClass objB = new MyObjectClass();

        /* ... */

        if (...)
        {
            objA.Insert();
        }
        else
        {
            objA.Delete();
            objB.Update();

            // Exception raised in objB.Update();
            // Data correspond to objA in database are lost
        }
    }

    private void WithTransaction()
    {
        MyObjectClass objA = new MyObjectClass();
        MyObjectClass objB = new MyObjectClass();

        sqlConn.Transaction(
            () => {
                if (...)
                {
                    objA.Insert();
                }
                else
                {
                    objA.Delete();
                    objB.Update(); // Exception raised in objB.Update()
                }
            },
            CallErrorLogFunction()
        );
    }
}

My problem : How i can know, when I use GetDataSet(), ExecuteQuery() or whatever, that I'm in a transaction function ?

  • StackTrace and StackFrame indicate invocation but not the source function of invocation.
  • I can't use Thread (for current thread ID) because my colleagues could make operations on the GUI.
  • I thought to lock instruction to check if in a transaction but I no idea to implement the code

If you have an idea ... :)

Thanks !


EDIT March 19'21 :

public class MySqlClass
{
    // They are initialized before use "ExecuteQuery()"
    public MyTransactionClass transac; 
    public SqlConnection conn;

    public void ExecuteQuery(string query, Dictionary<string, object> dict = null)
    {
        SqlDataAdapter da;

        try
        {
            if (conn.State = ConnectionState.Closed)
            {
                conn.Open();
            }

            da = new SqlDataAdapter(query, conn);

            /* section of code for parameters queries */
            if (dict != null) { /* ... */ }

            if (IsTransaction())
            {
                da.SelectCommand.Transaction = transac.TransacSql;
            }

            da.SelectCommand.ExecuteNonQuery();
        }
        catch (Exception)
        {
            throw;
        }
        finally
        {
            if (da != null)
            {
                da.Dispose();
            }
        }
    }

    public DataSet GetDataSet(string query) { /* ... */ }

    private bool IsTransaction()
    {
        bool r = false;

        if (transac != null)
        {
            /*
             * Check if in StackTrace, "Transaction" of "MyTransactionClass" is in the list
             */
            StackTrace st = new StackTrace(true);
            StackFrame sf;

            for (int i = 0; i < st.FrameCount; i++)
            {
                sf = st.GetFrame(i);

                if (sf.GetMethod().Name == MyTransactionClass.FunctionName_Transaction)
                {
                    r = true;
                    break;
                }
            }
        }

        return r;
    }
}

public class MyTransactionClass
{
    public static readonly string FunctionName_Transaction = nameof(Transaction);
    public SqlTransaction TransacSql;

    public void Transaction(Action queryToCommit, Action whenRollback)
    {
        CancellationTokenSource cts = new CancellationTokenSource();
        try
        {
            Task executeAction = new Task(new Action(() =>
            {
                queryToCommit.Invoke();
                cts.Token.ThrowIfCancellationRequested();
            }),
                cts.Token
                );

            executeAction.Start();
            executeAction.Wait(cts.Token);

            Commit();
        }
        catch (Exception)
        {
            Rollback();
            whenRollback.Invoke();

            /* ... */
        }
    }

    void Commit() { }
    void Rollback() { }
}

EDIT March 31'21 :

I make a unit test, with different use cases :

  • with just a transaction
  • a transaction and a Action
  • a transaction with a Action in a Task

Is the last case (similar to I want), System.Transactions.Transaction.Current is null.

[TestMethod]
public void TestTransactionScope()
{
    Console.WriteLine($"Transaction ID : {System.Transactions.Transaction.Current?.TransactionInformation.LocalIdentifier}");

    using (TransactionScope scope = new TransactionScope())
    {
        Console.WriteLine($"(scope) Transaction ID : {System.Transactions.Transaction.Current?.TransactionInformation.LocalIdentifier}");
        MyFunction("direct call");
    }

    // if I use a invocation
    Action<string> fnctTransac = new Action<string>((msg) => MyFunction(msg));
    using (TransactionScope scope = new TransactionScope())
    {
        Console.WriteLine($"(scope) Transaction ID : {System.Transactions.Transaction.Current?.TransactionInformation.LocalIdentifier}");
        fnctTransac.Invoke("with invocation");
    }

    // if I use invocation with a Task, similar to my use
    using (TransactionScope scope = new TransactionScope())
    {
        Console.WriteLine($"(scope) Transaction ID : {System.Transactions.Transaction.Current?.TransactionInformation.LocalIdentifier}");
        Task t = new Task(() => fnctTransac.Invoke("with invocation, from a Task"));
        t.Start();
        Task.WaitAll(t);
    }
}

public void MyFunction(string msg)
{
    Console.WriteLine($"{msg} - Transaction ID : {System.Transactions.Transaction.Current?.TransactionInformation.LocalIdentifier}");
}

Logs :

Transaction ID : 
(scope) Transaction ID : 1c0127fe-44d5-4954-826c-ece6ad261ee5:1
direct call - Transaction ID : 1c0127fe-44d5-4954-826c-ece6ad261ee5:1
(scope) Transaction ID : 1c0127fe-44d5-4954-826c-ece6ad261ee5:2
with invocation - Transaction ID : 1c0127fe-44d5-4954-826c-ece6ad261ee5:2
(scope) Transaction ID : 1c0127fe-44d5-4954-826c-ece6ad261ee5:3
with invocation, from a Task - Transaction ID : 

EDIT April 16'21

With TransactionScope :

Source :

public class MySqlClass
{
    public MyTransactionClass transac; 
    public SqlConnection conn;

    public void ExecuteQuery(string query, Dictionary<string, object> dict = null)  { /* ... */ }

    public DataSet GetDataSet(string query) { /* ... */ }

    private bool IsTransaction()
    {
        bool r = false;
        string idTransaction = Transactions.Transaction.Current?.TransactionInformation.LocalIdentifier

        if (
            transac == null &&
            !string.IsNullOrEmpty(idTransaction)
            )
        {
            transac = MyTransactionClass.GetTransaction(idTransaction)
        }

        return r;
    }
}

public class MyTransactionClass
{
    public const int MAX_TIME_TRANSAC_SEC = 5
    
    public static readonly string FunctionName_Transaction = nameof(Transaction);
    public SqlTransaction TransacSql;

    public static void Transaction(Action queryToCommit, Action whenRollback)
    {
        CancellationTokenSource cts = new CancellationTokenSource();
        try
        {
            Task executeAction = new Task(new Action(() =>
            {
                queryToCommit.Invoke();
                cts.Token.ThrowIfCancellationRequested();
            }),
                cts.Token
                );
                
            using (TransactionScope scope = new TransactionScope(
                TransactionScopeOption.Required,
                New TimeSpan(0, 0, MAX_TIME_TRANSAC_SEC),
                TransactionScopeAsyncFlowOption.Enabled
                )
            {
                executeAction.Start();
                executeAction.Wait(cts.Token);
                
                scope.Complete();
                Commit();
            }
        }
        catch (Exception)
        {
            Rollback();
            whenRollback.Invoke();

            /* ... */
        }
    }

    void Commit() { }
    void Rollback() { }
}

Tests :

[TestMethod]
public void E_TransactionScope()
{
    Console.WriteLine($"Transaction ID : {System.Transactions.Transaction.Current?.TransactionInformation.LocalIdentifier}");

    using (TransactionScope scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
    {
        Console.WriteLine($"(scope) Transaction ID : {System.Transactions.Transaction.Current?.TransactionInformation.LocalIdentifier}");
        MyFunction("direct call");
    }

    Console.WriteLine();

    // if I use a invocation
    Action<string> fnctTransac = new Action<string>((msg) => MyFunction(msg));
    using (TransactionScope scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
    {
        Console.WriteLine($"(scope) Transaction ID : {System.Transactions.Transaction.Current?.TransactionInformation.LocalIdentifier}");
        fnctTransac.Invoke("with invocation");
    }

    Console.WriteLine();

    // if I use invocation with a Task, similar to my use
    using (TransactionScope scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
    {
        Console.WriteLine($"(scope) Transaction ID : {System.Transactions.Transaction.Current?.TransactionInformation.LocalIdentifier}");
        Task t = new Task(() => fnctTransac.Invoke("with invocation, from a Task"));
        t.Start();
        Task.WaitAll(t);
    }

    Console.WriteLine();

    // ultimate use case
    Action userCode = () => MyFunction_First("last use case");
    Task tk;
    using (TransactionScope scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
    {
        Console.WriteLine($"(scope) Transaction ID : {System.Transactions.Transaction.Current?.TransactionInformation.LocalIdentifier}");

        // TransactionID not available when userCode is defined out of using instruction and called directly
        // userCode.Start();

        tk = new Task(new Action(() => userCode.Invoke()));
        tk.Start();
        tk.Wait();
    }

    Console.WriteLine("-------------------------");
}

public void MyFunction(string msg)
{
    Console.WriteLine($"{msg} - Transaction ID : {System.Transactions.Transaction.Current?.TransactionInformation.LocalIdentifier}");
}

public void MyFunction_First(string msg)
{
    Console.WriteLine($"MyFunction_First - {msg} - Transaction ID : {System.Transactions.Transaction.Current?.TransactionInformation.LocalIdentifier}");
    Task t = new Task(() => MyFunction_Second(msg));
    t.Start();
    Task.WaitAll(t);
}

public void MyFunction_Second(string msg)
{
    Console.WriteLine($"MyFunction_Second - {msg} - Transaction ID : {System.Transactions.Transaction.Current?.TransactionInformation.LocalIdentifier}");
    Thread th = new Thread(new ThreadStart(() => MyFunction_Third(msg)));
    th.Start();
    th.Join();
}

public void MyFunction_Third(string msg)
{
    using (TransactionScope scope = new TransactionScope())
    {
        Console.WriteLine($"MyFunction_Third - {msg} - Transaction ID : {System.Transactions.Transaction.Current?.TransactionInformation.LocalIdentifier}");
        Task t = new Task(() => MyFunction_Fourth(msg));
        t.Start();
        Task.WaitAll(t);
    }
}

public void MyFunction_Fourth(string msg)
{
    Console.WriteLine($"MyFunction_Fourth - {msg} - Transaction ID : {System.Transactions.Transaction.Current?.TransactionInformation.LocalIdentifier}");
    Task t = new Task(() => MyFunction_Last(msg));
    t.Start();
    Task.WaitAll(t);
}

public void MyFunction_Last(string msg)
{
    Console.WriteLine($"MyFunction_Last - {msg} - Transaction ID : {System.Transactions.Transaction.Current?.TransactionInformation.LocalIdentifier}");
}

Logs :

Transaction ID : 
(scope) Transaction ID : 99e1d658-27d8-404d-b57e-d6ead2e2308e:1
direct call - Transaction ID : 99e1d658-27d8-404d-b57e-d6ead2e2308e:1

(scope) Transaction ID : 99e1d658-27d8-404d-b57e-d6ead2e2308e:2
with invocation - Transaction ID : 99e1d658-27d8-404d-b57e-d6ead2e2308e:2

(scope) Transaction ID : 99e1d658-27d8-404d-b57e-d6ead2e2308e:3
with invocation, from a Task - Transaction ID : 99e1d658-27d8-404d-b57e-d6ead2e2308e:3

(scope) Transaction ID : 99e1d658-27d8-404d-b57e-d6ead2e2308e:4
MyFunction_First - last use case - Transaction ID : 99e1d658-27d8-404d-b57e-d6ead2e2308e:4
MyFunction_Second - last use case - Transaction ID : 99e1d658-27d8-404d-b57e-d6ead2e2308e:4
MyFunction_Third - last use case - Transaction ID : 99e1d658-27d8-404d-b57e-d6ead2e2308e:4
MyFunction_Fourth - last use case - Transaction ID : 
MyFunction_Last - last use case - Transaction ID : 
vek
  • 11
  • 4
  • You haven't provided any code for `MySqlClass` so how can we know? We don't even know if you are using ADO.NET directly or are you using a mapper. I can tell you that if you are caching a connection object you are probably doing something wrong – Charlieface Mar 18 '21 at 14:20
  • 2
    I'm a little confused. You said "I want use transaction WITHOUT modify originals functions." If you can't modify the functions, what piece of code need to "know" that it's in a transaction, if not the transaction function itself (which obviously knows)? – John Wu Mar 18 '21 at 23:10
  • @Charlieface : why caching a connection object is bad ? This connection is closed by the app when is closed. @JohnWu : when I want execute query, I see if a transaction is in progress, with **IsTransaction()** in ``MySqlClass``. It's for this that use StackTrace and StackFrame, to see if **Transaction** (of ``MyTransactionClass``) is a source – vek Mar 19 '21 at 10:46
  • Because you are holding open the connection which uses up resources on the server. Furthermore, it is much harder to ensure it is closed correctly without a `using` block, it requires careful usage of `IDisposable` and `using` on containing objects, which I rarely see in beginner or even intermediate code. See also https://stackoverflow.com/questions/17552829/c-sharp-data-connections-best-practice – Charlieface Mar 19 '21 at 10:49
  • @Charlieface : I know but I construct and enhance functions slowly. I use often ``using`` in my code and is better of this ... utilization, I recognize. – vek Mar 19 '21 at 10:53
  • I guess I don't understand why you are effectively duplicating all the functionality of a command/adapter object, they already have `Connection` and `Transaction` properties – Charlieface Mar 19 '21 at 11:10

1 Answers1

0

Have you already considered using the TransactionScope class?

I'm not sure, but with that, you might not need to know whether you are in a transaction or not inside your ExecuteQuery and GetDataSet methods. It might just simply work. ;)

Your Transaction method could look something like this:

public void Transaction(Action queryToCommit, Action whenRollback = null)
{
    if (queryToCommit == null)
    {
        throw new ArgumentNullException(nameof(queryToCommit));
    }

    CancellationTokenSource cts = new CancellationTokenSource();
    Task executeAction = new Task(new Action(() =>
        {
            queryToCommit.Invoke(); // user code executed here
            cts.Token.ThrowIfCancellationRequested();
        }),
        cts.Token
        );

    try
    {
        using (var scope = new System.Transactions.TransactionScope())
        {
            executeAction.Start();
            executeAction.Wait(cts.Token);

            scope.Complete();
        }
    }
    catch (Exception)
    {
        whenRollback.Invoke();
        /* ... */
    }
}

Note that I haven't tested this. I just based it on your code and the sample in the documentation, so it probably needs some additional work.

Edit:

Based on recent comments, I added a code snippet that shows how I would personally implement your logic:

public class MySqlClass
{
    private string connectionString;

    public MySqlClass(string connectionString)
    {
        this.connectionString = connectionString;
    }

    public int ExecuteQuery(string query, params SqlParameter[] parameters)
    {
        using (SqlConnection con = new SqlConnection(connectionString))
        using (SqlCommand cmd = new SqlCommand(query, con))
        {
            con.Open();
            cmd.Parameters.AddRange(parameters);

            return cmd.ExecuteNonQuery();
        }
    }

    public DataSet GetDataSet(string query, params SqlParameter[] parameters)
    {
        using (SqlConnection con = new SqlConnection(connectionString))
        using (SqlCommand cmd = new SqlCommand(query, con))
        {
            con.Open();
            cmd.Parameters.AddRange(parameters);

            DataSet ds = new DataSet();

            using (SqlDataAdapter da = new SqlDataAdapter(cmd))
            {
                da.Fill(ds);
            }

            return ds;
        }
    }

    public DataTable GetDataTable(string query, params SqlParameter[] parameters)
    {
        DataSet ds = GetDataSet(query, parameters);
        return ds.Tables[0];
    }

    public static void Transaction(Action queryToCommit, Action whenRollback)
    {
        if (queryToCommit == null)
        {
            throw new ArgumentNullException(nameof(queryToCommit));
        }

        CancellationTokenSource cts = new CancellationTokenSource();
        Task executeAction = new Task(new Action(() =>
            {
                queryToCommit.Invoke(); // user code executed here
                cts.Token.ThrowIfCancellationRequested();
            }),
            cts.Token
            );

        try
        {
            using (var scope = new System.Transactions.TransactionScope())
            {
                executeAction.Start();
                executeAction.Wait(cts.Token);

                scope.Complete();
            }
        }
        catch
        {
            whenRollback.Invoke();
            /* ... */
        }
    }

    public static SqlParameter CreateParameter(string parameterName, SqlDbType dbType, object value)
    {
        SqlParameter parameter = new SqlParameter(parameterName, dbType);
        parameter.Value = value;
        return parameter;
    }

    public static SqlParameter CreateParameter(string parameterName, SqlDbType dbType, int size, object value)
    {
        SqlParameter parameter = new SqlParameter(parameterName, dbType, size);
        parameter.Value = value;
        return parameter;
    }
}

/*
CREATE TABLE [MyObjects] (
    [Id] INT IDENTITY NOT NULL,
    [Color] VARCHAR(20) NOT NULL,
    --...
    CONSTRAINT [PK_MyObjects] PRIMARY KEY ([Id])
);
*/

public class MyObjectModel
{
    // Sample private fields
    public int? Id { get; set; }
    public string Color { get; set; }
    /* ... */
}

public class MyObjectRepository
{
    private MySqlClass sqlConn;

    public MyObjectRepository(MySqlClass sqlConn)
    {
        this.sqlConn = sqlConn;
    }

    public List<MyObjectModel> GetMyObjects(string colorFilter)
    {
        List<MyObjectModel> obj = new List<MyObjectModel>();

        string query = (string.IsNullOrWhiteSpace(colorFilter))
            ? "SELECT [Id], [Color] FROM [MyObjects];"
            : "SELECT [Id], [Color] FROM [MyObjects] WHERE [Color] LIKE '%' + @Color + '%';";

        using (DataTable dt = sqlConn.GetDataTable(query,
            MySqlClass.CreateParameter("@Color", SqlDbType.VarChar, 20, colorFilter)))
        {
            foreach (DataRow dr in dt.Rows)
            {
                obj.Add(ConvertDataRow(dr));
            }
        }

        return obj;
    }

    private static MyObjectModel ConvertDataRow(DataRow dr)
    {
        return new MyObjectModel
        {
            /* set class properties here... */
            Id = dr.Field<int>("Id"),
            Color = dr.Field<string>("Color")
            /* ... */
        };
    }

    public void Insert(MyObjectModel model)
    {
        if (model.Id.HasValue)
        {
            return;
        }

        using (DataTable dt = sqlConn.GetDataTable("INSERT INTO [MyObjects] ([Color]) OUTPUT INSERTED.[Id] VALUES (@Color);",
            MySqlClass.CreateParameter("@Color", SqlDbType.VarChar, 20, model.Color)))
        {
            model.Id = (int)dt.Rows[0]["Id"];
        }
    }

    public int? Update(MyObjectModel model)
    {
        if (!model.Id.HasValue)
        {
            return null;
        }

        return sqlConn.ExecuteQuery("UPDATE [MyObjects] SET [Color] = @Color WHERE [Id] = @Id;",
            MySqlClass.CreateParameter("@Id", SqlDbType.Int, model.Id),
            MySqlClass.CreateParameter("@Color", SqlDbType.VarChar, 20, model.Color));
    }

    public int Delete(int id)
    {
        return sqlConn.ExecuteQuery("DELETE FROM [MyObjects] WHERE [Id] = @Id;",
            MySqlClass.CreateParameter("@Id", SqlDbType.Int, id));
    }
}

public class MyApp
{
    private MyObjectRepository repo;

    public MyApp()
    {
        MySqlClass sqlConn = new MySqlClass(" ... connection string here ... ");
        repo = new MyObjectRepository(sqlConn);
    }

    List<MyObjectModel> GetMyObjects(string colorFilter = "white")
    {
        return repo.GetMyObjects(colorFilter);
    }

    private void WithoutTransaction()
    {
        MyObjectModel objA = new MyObjectModel();
        MyObjectModel objB = new MyObjectModel();

        /* ... */

        if (true)
        {
            repo.Insert(objA);
        }
        else
        {
            repo.Delete(objA.Id.Value);
            repo.Update(objB);

            // Exception raised in objB.Update();
            // Data correspond to objA in database are lost
        }
    }

    private void WithTransaction()
    {
        MyObjectModel objA = new MyObjectModel();
        MyObjectModel objB = new MyObjectModel();

        MySqlClass.Transaction(
            () =>
            {
                if (true)
                {
                    repo.Insert(objA);
                }
                else
                {
                    repo.Delete(objA.Id.Value);
                    repo.Update(objB); // Exception still raised now???
                }
            },
            CallErrorLogFunction
        );
    }

    private void CallErrorLogFunction()
    {
        /* ... */
    }
}

From recent edits to your question, I am not sure if this would be a valid solution, however. This is the best I could come up with so far. Perhaps you can extract something useful from it anyway. (I haven't tested it, so despite me being careful, the code might contain some stupid mistakes.)

Bart Hofland
  • 3,700
  • 1
  • 13
  • 22
  • I didn't know ``TransactionScope``, I will look into it. Thanks ! – vek Mar 19 '21 at 10:55
  • Sorry for the delay. I tested `TransactionScope` and it can be answer to my problem ... if I can know that I am in a Transaction. I use `Console.WriteLine($"Transaction ID : {Transactions.Transaction.Current.TransactionInformation.LocalIdentifier}")` (return **Transaction ID : 4df77aa7-6e27-4372-aa31-f7eaec29fad0:1**) after instanciation of scope. But in my function `IsTransaction()` (edition of March 19'21), I can't obtain ID of current transaction (**Transactions.Transaction.Current = null**). If instanciate a new TransactionScope, the last caracter (after `:`) is incremented. – vek Mar 30 '21 at 15:18
  • Hi @vek. No problem for the delay. :) I have to admit that I have very little experience with the TransactionScope functionality myself as well. I am wondering, however, for what reason you need to know if you are inside a transaction or not. As far as I can see, it would not matter for the queries that you execute (using the `ExecuteQuery` and/or `GetDataSet` methods). But I might be mistaken, of course. Can you perhaps provide an example in which case you really need to know that you are inside a transaction or not? And if so, what would you actually need to do differently in such a case? – Bart Hofland Mar 30 '21 at 18:37
  • @vek . . . Ah. I just saw that edit from March 19 '21. Sorry.. . Interesting. What would happen if you completely drop that `if`-block in your `ExecuteQuery` method that sets the `Transaction` property of the `SelectCommand` of that `DataAdapter`? I'm not entirely sure, but I think you don't need it. I bet that TransactionScope will handle a transaction just fine here as well. – Bart Hofland Mar 30 '21 at 19:01
  • If I drop **if-block** (in any case, `IsTransaction()` return always `false`), I have an exception when I execute an INSERT query (with `da.SelectCommand.ExecuteNonQuery()` : `ExecuteNonQuery implies that the command has a transaction when the connection assigned to the command is in a pending local transaction. The Transaction property of the command has not been initialized.` (translated from french) – vek Mar 31 '21 at 07:52
  • I see. You are re-using your connections. I didn't notice that before. Perhaps you should consider to create and open a new connection object and assign it to a local variable (in a `using` block/statement) each time you require it within `ExecuteQuery` and `GetDataSet`. ADO.NET will probably use [connection pooling](https://learn.microsoft.com/en-us/dotnet/framework/data/adonet/connection-pooling) in the background to optimize reuse of connections. – Bart Hofland Mar 31 '21 at 08:07
  • Things are getting pretty complicated right now, it seems. Sorry about that. When I posted my answer, I wasn't aware that it would require a thorough "redesign" of your code. I will try to edit my answer to include a code snippet for your `MySqlClass` as I would implement it. – Bart Hofland Mar 31 '21 at 08:10
  • I edited my question to add simulate use cases. – vek Mar 31 '21 at 09:35
  • I edited my first post with my final solution : TransactionScope. It's possible to know if I am in a Transaction with some parameters, as ```TransactionScopeAsyncFlowOption.Enabled```. I was forced to keep my class because is used in many applications (but I save your code for a next version ;-) ) I tested with many use case (threads, Action, Action + thread, ...) and TransactionId is lost only when I make an another TransactionScope (it's unlikely for me). – vek Apr 16 '21 at 14:10