2

I don't see why I have to create a DbCommand object every time I need to call a stored procedure. So I'm trying to come up with a way to do that. I have tested my code (see below). But I would like to check with the community in case there is something I have missed. I would be using it with in an ASP.NET app. Is this code thread safe?

SharedDbCommand - wraps up the creation and storage of the DbCommand object

Db - the wrapper for the database, uses the SharedDbCommand via a static field and the ThreadStatic attribute

Program - the console app that starts threads and uses the Db object which

// SharedDbCommand.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
using System.Data.Common;
using System.Data.SqlClient;
using System.Data;

namespace TestCmdPrepare {
    public class SharedDbCommand {
        [ThreadStatic]
        static DbCommand cmd;

        public SharedDbCommand(string procedureName, ConnectionStringSettings dbConfig) {
            var factory = DbProviderFactories.GetFactory(dbConfig.ProviderName);
            cmd = factory.CreateCommand();
            cmd.Connection = factory.CreateConnection();
            cmd.Connection.ConnectionString = dbConfig.ConnectionString;
            cmd.CommandText = procedureName;
            cmd.CommandType = System.Data.CommandType.StoredProcedure;
            if (cmd is SqlCommand) {
                try {
                    cmd.Connection.Open();
                    SqlCommandBuilder.DeriveParameters(cmd as SqlCommand);
                } finally {
                    if (cmd != null && cmd.Connection != null) 
                        cmd.Connection.Close();
                }
            }
        }

        public DbParameter this[string name] {
            get {
                return cmd.Parameters[name];
            }
        }

        public IDataReader ExecuteReader() {
            try {
                cmd.Connection.Open();
                return cmd.ExecuteReader(CommandBehavior.CloseConnection);
            } finally {
                cmd.Connection.Close();
            }
        }

        public void ExecuteNonQuery() {
            try {
                cmd.Connection.Open();
                cmd.ExecuteNonQuery();
            } finally {
                cmd.Connection.Close();
            }
        }
    }
}

// Db.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
using System.Data.Common;
using System.Data;
using System.Data.SqlClient;
using System.Threading;
using System.Diagnostics;

namespace TestCmdPrepare {
    public class Db {
        ConnectionStringSettings dbSettings;
        DbProviderFactory factory;
        public Db() {
            dbSettings = ConfigurationManager.ConnectionStrings["db"];
            factory = DbProviderFactories.GetFactory(dbSettings.ProviderName);
        }
        IDataReader ExecuteReader(DbCommand cmd) {
            cmd.Connection.Open();
            return cmd.ExecuteReader(CommandBehavior.CloseConnection);
        }

        private DbConnection CreateConnection() {
            var c = factory.CreateConnection();
            c.ConnectionString = dbSettings.ConnectionString;
            return c;
        }

        DbCommand CreateCommand(string procedureName) {
            var cmd = factory.CreateCommand();
            cmd.Connection = CreateConnection();
            cmd.CommandText = "get_stuff";
            cmd.CommandType = CommandType.StoredProcedure;
            if (cmd is SqlCommand) {
                try {
                    cmd.Connection.Open();
                    SqlCommandBuilder.DeriveParameters(cmd as SqlCommand);
                } finally {
                    cmd.Connection.Close();
                }
            }
            return cmd;
        }


        [ThreadStatic]
        static DbCommand get_stuff;

        DbCommand GetStuffCmd {
            get {
                if (get_stuff == null)
                    get_stuff = CreateCommand("get_stuff");
                return get_stuff;
            }
        }

        public string GetStuff(int id) {
            GetStuffCmd.Parameters["@id"].Value = id;
            using (var reader = ExecuteReader(GetStuffCmd)) {
                if (reader.Read()) {
                    return reader.GetString(reader.GetOrdinal("bar"));
                }
            }
            return null;
        }

        [ThreadStatic]
        static SharedDbCommand get_stuff2;
        public string GetStuff2(int id) {
            if (get_stuff2 == null)
                get_stuff2 = new SharedDbCommand("get_stuff", dbSettings);
            get_stuff2["@id"].Value = id;
            using (var reader = get_stuff2.ExecuteReader()) {
                if (reader.Read()) {
                    Thread.Sleep(1000);
                    return reader.GetString(reader.GetOrdinal("bar"));
                }
            }
            return null;
        }
    }
}


// Program.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Common;
using System.Configuration;
using System.Data.SqlClient;
using System.Threading;

namespace TestCmdPrepare {
    class Program {
        static void Main(string[] args) {
            var db = new Db();
            var threads = new List<Thread>();
            for (int i = 0; i < 4; i++) {
                var one = new Thread(Run2);
                var two = new Thread(Run1);

                threads.Add(one);
                threads.Add(two);
                one.Start();
                two.Start();

                Write(db, 1);
                Write(db, 2);
            }
            Console.WriteLine("Joining");
            foreach (var thread in threads) {
                thread.Join();
            }
            Console.WriteLine();
            Console.WriteLine("DONE");
            Console.ReadLine();
            return;
        }

        static void Write(Db db, int id) {


       Console.WriteLine("2:{0}:{1}", Thread.CurrentThread.ManagedThreadId, db.GetStuff2(id));
        Console.WriteLine("1:{0}:{1}", Thread.CurrentThread.ManagedThreadId, db.GetStuff(id));
    }

    static void Run1() {
        var db = new Db();
        Write(db, 1);
    }

    static void Run2() {
        var db = new Db();
        Write(db, 2);
    }

    }
}
2
  • Are you going to use one connection for all threads/users/sessions? Commented Apr 11, 2011 at 6:07
  • no, i expect the command object on each thread to have its own connection object. I want to be able to create as many Db objects as i want but only create the command object once. I don't mind that one gets created for each thread because I don't want to lock on access to the command object Commented Apr 11, 2011 at 6:26

2 Answers 2

1

Bad idea for lots of reasons. Others have mentioned some of them, but I'll give you one specific to your implementation: using ThreadStatic in ASP.NET will bite you eventually (see here). You don't control the pipeline, so it's possible for multiple threads to end up servicing one request (think between event handlers on a page, etc- how much code is running that isn't yours?). It's also easy to orphan data on a request thread you don't own due to unhandled exceptions. Might not be a showstopping problem, but best case you're looking at a memory leak and increased server resource usage while your connection just sits there...

I'd recommend you just let the ConnectionPool do its job- it has some warts, but performance isn't one of them compared to what else is going on in the ASP.NET pipeline. If you really want to do this, at least consider storing the connection object in HttpContext.Current.Items. You can probably make this work in limited circumstances, but there will always be gotchas, especially if you ever start writing parallel code.

Just $0.02 from a guy who's been there. :)

Sign up to request clarification or add additional context in comments.

4 Comments

To make sure I understand you, your saying that, for example, after I've set all my parameter values on my command object, ASP.NET could recycle that thread, and some other executing page pick up that thread and start using it at the same point. Then load up a stack and start executing on that thread. BUT, since i have the command object ThreadStatic that the newly executing code will be using the wrong command object?
Theoretically possible, yes. Likely on current ASP.NET with non-exotic page impls? Probably not... Google around, though- the use of ThreadStatic in environments where you don't own the threads is generally regarded as a Bad Idea. Plus, you're just making your life difficult- I'll bet a box of cookies that DB connection overhead isn't anywhere close to the biggest bottleneck in your app. With millions of devs writing on ASP.NET, if something like this were necessary, don't you think it would already be out there?
You would win that box of cookies if I were dumb enough to bet you. I expected this idea to get trampled on, for the reason you said, (wouldn't everyone be doing it?). However, in my defense, no one ever did anything ground breaking by following the herd.
Curses! Foiled again! You win this time ASP.NET, but we will meet again.
0

This is not a good practice to keep DbCommand created. Also, this makes your application very complex because of thread handling logic.

As Microsoft suggests you should create and dispose connection and command objects right after you have executed your query. They are very lightweight to create. No need to keep memory used with them - dispose them when your query execution is complete and you have fetched all of the results.

3 Comments

My example code is supposed to be thread safe for calling code. If there are any specific threading issues I have missed, could you please point them out? I am fully aware of the concept of cleaning up unmanaged resources. If you have a link that has details on why I should create and dispose right away for reasons other than basic unmanaged resource clean up could you please post that? To the best of my knowledge, calling Close() on the connection object releases the only unmanaged resource involved with calling the database.
Please, take a look at MSDN article regarding Connection Pooling. It is recommended that you always close the Connection when you are finished using it in order for the connection to be returned to the pool. This can be done using either the Close or Dispose methods of the Connection object. Connections that are not explicitly closed might not be added or returned to the pool. For example, a connection that has gone out of scope but that has not been explicitly closed will only be returned to the connection pool if the maximum pool size has been reached and the connection is still valid.
Your comment did make me review my code and locate a missed Close() on the connection, thank you. I am fully aware that this example code is unconventional. Its purpose is to remove clock cycles in exchange for less memory. I'm not asking what is the proper way to call a stored procedure. I've been doing that for well over a decade now. I'm asking about the thread safety of the code. Is it possible for two processes to stomp on each other in here somewhere, and if so, why?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.