0

I have the following DAL class hierarchy (partially shown), which I use for abstracting data access to the database. I would like to use it in a thread-safe manner:

public class DbAdapter : IDbAdapter, IDisposable
{
    private SqlConnection _conn;
    private readonly string _connString;

    protected DbAdapter(string connString)
    {
        if (string.IsNullOrWhiteSpace(connString))
            throw new ArgumentException("Value cannot be null, empty or whitespace", nameof(connString));
        _connString = connString;
    }

    public void Dispose()
    {
        CloseConnection();
    }

    public SqlConnection GetConnection()
    {

        if (_conn == null || _conn.State == ConnectionState.Closed)
            _conn = new SqlConnection(_connString);
        else if (_conn.State == ConnectionState.Broken)
        {
            _conn.Close();
            _conn.Open();
        }
        return _conn;
    }

    public void CloseConnection()
    {
        if (_conn != null && _conn.State == ConnectionState.Open)
            _conn.Close();
        _conn = null;
    }

    public SqlCommand GetCommand(string query, SqlTransaction transaction)
    {
        var cmd = new SqlCommand
        {
            Connection = transaction != null ? transaction.Connection : GetConnection()
        };
        if (transaction != null)
            cmd.Transaction = transaction;
        cmd.CommandType = CommandType.Text;
        cmd.CommandText = query;
        cmd.CommandTimeout = 500;
        return cmd;
    }
    
    // Omitted other methods
}

public class PersonDba : DbAdapter 
{
    //Omitted 

    public IList<Person> GetPersons(string whereClause, string orderClause, SqlTransaction transaction = null)
    {
        var query = "SELECT Id, Name, PersonId, Birthdate, Modified FROM Persons ";

        if (!string.IsNullOrWhiteSpace(whereClause))
            query += whereClause;
        if (!string.IsNullOrWhiteSpace(orderClause))
            query += orderClause;

        IList<Person> result = new List<Person>();

        var sqlCmd = GetCommand(query, transaction);
        using (var reader = sqlCmd.ExecuteReader(CommandBehavior.CloseConnection))
        {
            while (reader.Read())
            {
                var person = new Person
                {
                    Id = reader.GetInt32(reader.GetOrdinal("Id")),
                    PersonId = reader.GetInt32(reader.GetOrdinal("PersonId")),
                    Name = reader.GetString(reader.GetOrdinal("Name")),
                    Birthdate = reader.GetDateTime(reader.GetOrdinal("Birthdate")),
                    LastModified = reader.GetDateTime(reader.GetOrdinal("Modified"))
                };
                result.Add(person);
            }
        }
        return result;
    }
}

Typically, I inject a single instance of PersonDba into other classes, which contain multi-threaded code. In order to avoid locking around all access to that single instance within dependant code, I am thinking of making the SQLConnection DbAdapter._conn of type ThreadLocal<SqlConnection> (see ThreadLocal). Would that be enough to ensure using instances of this class is thread-safe?

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Bahaa
  • 1,577
  • 18
  • 31
  • 7
    This is a bad abstraction because you're forcing consumers to construct `where` clauses as strings, which means that they can't use the most appropriate way of avoiding SQL injection issues - parameters. – Damien_The_Unbeliever Sep 24 '20 at 09:23
  • That's true. It's a legacy code problem, which I manage in upper layers. Nonetheless, this is not the point of the question. – Bahaa Sep 24 '20 at 09:28
  • [You shouldn't share a `SqlConnection` across threads](https://stackoverflow.com/a/21775057/585968). –  Sep 24 '20 at 09:38
  • The question you linked deals with something different. In my question I am NOT willing to share the SqlConnection across threads. On the contrary, I want to make it ThreadLocal, which gives every thread its own value. – Bahaa Sep 24 '20 at 09:40
  • 1
    You have a larger design problem. _Each thread should have it's own repo object and subsequent database connection_. Your `PersonDba` and base class is a strange way to do a repo pattern –  Sep 24 '20 at 09:46
  • Thought of that too. It would require refactoring for example to inject a PersonDbaFactory instead, which gives every spawned thread its instance. But, once again, I'm specifically asking whether making the instance variable `_conn` ThreadLocal would solve the problem – Bahaa Sep 24 '20 at 09:48
  • That's why these are _comments_ and not _answers_ –  Sep 24 '20 at 09:57
  • Is it possible to use the [`ExecuteReaderAsync`](https://learn.microsoft.com/en-us/dotnet/api/system.data.sqlclient.sqlcommand.executereaderasync) instead of the `ExecuteReader` in the future, or we can assume that your data access layer will remain synchronous forever? – Theodor Zoulias Sep 24 '20 at 09:57
  • Yes, @TheodorZoulias, it's possible – Bahaa Sep 24 '20 at 10:04
  • 3
    This is mostly subjective, but I would *strongly advise against this*, not least because it won't work at all with `async`, where you can be jumping all over the place in terms of threads. OK, you can side-step that by using async-local or the execution-context to store the ambient data, but it would *still* be a very bad idea. IMO: pass it in explicitly, like you are with the transaction (in fact, since a transaction and connection are deeply connected, passing them in different ways should be a huge red flag) – Marc Gravell Sep 24 '20 at 10:08
  • 2
    Sure, you can make it thread-local. Strongly consider biting the bullet and refactoring this stuff, though -- a first start could be to make `CloseConnection`/`GetCommand` accept it as a parameter, and start rewriting callers up the chain to pass the connection they got. This can then be easily extended to make them use proper `using` patterns. This design is already broken; adding thread-local storage is just going to make things more confusing and unreliable, not less. – Jeroen Mostert Sep 24 '20 at 10:09
  • 3
    Side note: you're doing a lot of work here that tools like "Dapper" could do for you much more simply; also - a where/order-by clause as a string is just asking for SQL injection – Marc Gravell Sep 24 '20 at 10:11

1 Answers1

1

Assuming that you want to keep open the possibility of making your DAL asynchronous in the future, by using the ExecuteReaderAsync instead of the ExecuteReader, then converting the field _conn of the class DbAdapter to ThreadLocal<SqlConnection> is not safe. The reason is that after awaiting the asynchronous request, the asynchronous workflow will most probably continue on another ThreadPool thread. So the wrong connection will be closed, and some other unrelated concurrent data access operation may be interrupted and aborted.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • I am considering a *non-static* ThreadLocal _conn variable. Also, currently the code is synchronous, could as well stay so, if thread-safe execution can be guaranteed in consuming classes. – Bahaa Sep 24 '20 at 10:18
  • 1
    @Bahaa If you are OK with being constrained in the synchronous model forever, then the `ThreadLocal` is safe. Yes, making it static doesn't make sense, because you may want to maintain multiple connections per thread, for different databases. I'll update my answer. – Theodor Zoulias Sep 24 '20 at 10:27