Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / database / SQL-Server

Generator-less! C# wrapper for SQL Stored Procedures

4.62/5 (23 votes)
20 Sep 2005CPOL3 min read 1  
Eliminate the pain of calling SQL stored procedures from C#.

Introduction

If you've dealt with SQL from C#, you know how contrived it is. A whole lot of repetition that begs for a better way. Several on this site have already documented the pain and submitted a "better" way here, here, and here. These solutions involve an intermediate code generation step that requires you to paste in a bunch of code for each sproc (stored procedure), which allows you to then call the sproc like a normal procedure from C#. This extra code generation step, while kinda cool, is not without pain. The solutions are somewhat unwieldy, and some involve external dependencies.

A better way

There's a way to call an sproc from C# almost like a regular procedure, without any intermediate generating, pasting, or dependencies, and with reasonable performance (sproc info is cached). Here's how you might use the code:

C#
MySqlConn _sqlConn = new MySqlConn(
           "provide a valid connection string here");
...
_sqlConn.ExecSProc("AddStaff", staffName, isDoctor);
_sqlConn.ExecSProc("AddRoom", roomName);

Honest, that's it! You just called two sprocs; one named "AddStaff" with two input parameters, then "AddRoom" passing in one parameter. About the only drawback (compared to the code generation method) is that since parameters can be of any type, we won't know until runtime if there was a type mismatch, or if we passed the wrong number of parameters. Wah. Also note that neither this method nor automatic code generation will keep you from going stale - if you change the number or type or order of parameters in your sproc, you'll have to remember to update your C# code.

Does your sproc return a value and/or have output parameters? Well, if your sproc returns an error code and sets an out Param @roomId, then it might look like this:

C#
int err = _sqlConn.ExecSProc("AddRoom", roomName);
if (err == 0)    //all ok?
    roomId = (int)_sqlConn.Param("@roomId");

How much does it cost, you ask? About 130 lines of code that you can copy, paste, and forget about. Here it is:

C#
using System;
using System.Collections;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;

namespace CodeProjectRocks
{
    public class MySqlConn
    {
        SqlConnection _dbConn;
        SProcList  _sprocs;  //sproc parameter info cache
        SqlParameterCollection _lastParams; //used by Param()

        public MySqlConn(string connStr)
        {
            _dbConn = new SqlConnection(connStr);
            _sprocs = new SProcList(this);
        }

        void Open()   
         { if (_dbConn.State != ConnectionState.Open) _dbConn.Open(); }
        void Close()  
         { if (_dbConn.State == ConnectionState.Open) _dbConn.Close(); }

        SqlCommand NewSProc(string procName)
        {
            SqlCommand cmd = new SqlCommand(procName, _dbConn);
            cmd.CommandType = CommandType.StoredProcedure;

#if EmulateDeriveParameters   //see below for our 
                              //own DeriveParameters
            MySqlCmdBuilder.DeriveParameters(cmd);
#else
            Open();
            SqlCommandBuilder.DeriveParameters(cmd);
           //SQL treats OUT params as REF params 
           //(thus requiring those parameters to be passed in)
           //if that's what you really want, remove 
           //the next three lines
            foreach (SqlParameter prm in cmd.Parameters)
                if (prm.Direction == ParameterDirection.InputOutput)
                    //make param a true OUT param
                    prm.Direction = ParameterDirection.Output; 
#endif

            return cmd;
        }

        SqlCommand FillParams(string procName, 
                                params object[] vals)
        {
            //get cached info (or cache if first call)
            SqlCommand cmd = _sprocs[procName];  

            //fill parameter values for stored procedure call
            int i = 0;
            foreach (SqlParameter prm in cmd.Parameters)
            {
                //we got info for ALL the params - only 
                //fill the INPUT params
                if (prm.Direction == ParameterDirection.Input
                 || prm.Direction == ParameterDirection.InputOutput)
                    prm.Value = vals[i++];
            }
            //make sure the right number of parameters was passed
            Debug.Assert(i == (vals == null ? 0 : vals.Length));

            //for subsequent calls to Param()
            _lastParams = cmd.Parameters;    
            return cmd;
        }

        //handy routine if you are in control of the input.
        //but if user input, vulnerable to sql injection attack
        public DataRowCollection QueryRows(string strQry)
        {
            DataTable dt = new DataTable();
            new SqlDataAdapter(strQry, _dbConn).Fill(dt);
            return dt.Rows;
        }

        public int ExecSProc(string procName, 
                              params object[] vals)
        {
            int retVal = -1;  //some error code

            try
            {
                Open();
                FillParams(procName, vals).ExecuteNonQuery();
                retVal = (int)_lastParams[0].Value;
            }
            //any special handling for SQL-generated error here
            //catch (System.Data.SqlClient.SqlException esql) {}
            catch (System.Exception e)
            {
                //handle error
            }
            finally
            {
                Close();
            }
            return retVal;
        }

        public DataSet ExecSProcDS(string procName, 
                                     params object[] vals)
        {
            DataSet ds = new DataSet();

            try
            {
                Open();
                new SqlDataAdapter(
                      FillParams(procName, vals)).Fill(ds);
            }
            finally
            {
                Close();
            }
            return ds;
        }

        //get parameter from most recent ExecSProc
        public object Param(string param)
        {
            return _lastParams[param].Value;
        }

        class SProcList : DictionaryBase
        {
            MySqlConn _db;
            public SProcList(MySqlConn db)        
               { _db = db; }

            public SqlCommand this[string name]
            { get {      //read-only, "install on demand"
                if (!Dictionary.Contains(name))
                    Dictionary.Add(name, _db.NewSProc(name));
                return (SqlCommand)Dictionary[name];
            } }
        }
    }
}

As an FYI only to show how to query SQL for param info (you do not need this code, unless, as pointed out by a reader, you are inside a SQL transaction, which the framework apparently does not handle):

C#
#if EmulateDeriveParameters
    class MySqlCmdBuilder {
        static SqlTypeMap _sqlTypeMap = null;
        class SqlTypeMap : DictionaryBase {
            public SqlDbType this[string key]
              { get { return (SqlDbType)Dictionary[key]; }}
            public void Add(string key, SqlDbType value)  
              { Dictionary.Add(key, value); }
        }

        //static helper class - don't allow instantiation
        private MySqlCmdBuilder() {}    

        public static void DeriveParameters(SqlCommand cmd)
        {
            EnsureTypeMap();

            //cmd.Parameters[0] will always hold 
            //the sproc return value
            SqlParameter prmRet = 
                new SqlParameter("@RETURN_VALUE", SqlDbType.Int);
            prmRet.Direction = ParameterDirection.ReturnValue;
            cmd.Parameters.Add(prmRet);

            string qrySProc = 
                "SELECT parameter_name as name"
                    + ", data_type as xtype"
                    + ", cast(isnull(character_maximum_length, " + 
                                       "numeric_scale) as int) as prec"
                    + ", case when parameter_mode like '%out%' " + 
                                       "then 1 else 0 end as isoutparam"
                + " FROM INFORMATION_SCHEMA.PARAMETERS"
                + " WHERE specific_name = '" + cmd.CommandText + "'"
                + " ORDER BY ordinal_position";

            //query SQL-server for given sproc's parameter info
            DataTable dt = new DataTable(); 
            new SqlDataAdapter(qrySProc, cmd.Connection).Fill(dt);
            foreach (DataRow dr in dt.Rows)
            {
                SqlParameter prm = new SqlParameter(
                        (string)dr[0],               //dr["name"] 
                        _sqlTypeMap[(string)dr[1]],  //dr["xtype"]
                        (int)dr[2]);                 //dr["prec"]
                if ((int)dr[3] == 1)                 //isoutparam?
                    prm.Direction = ParameterDirection.Output;
                cmd.Parameters.Add(prm);
            }
        }

        static void EnsureTypeMap()
        {
            if (_sqlTypeMap == null) {
                _sqlTypeMap = new SqlTypeMap();
                _sqlTypeMap.Add("bit",          SqlDbType.Bit);
                _sqlTypeMap.Add("int",          SqlDbType.Int);
                _sqlTypeMap.Add("smallint",     SqlDbType.SmallInt);
                _sqlTypeMap.Add("tinyint",      SqlDbType.TinyInt);
                _sqlTypeMap.Add("datetime",     SqlDbType.DateTime);
                _sqlTypeMap.Add("smalldatetime",SqlDbType.SmallDateTime);
                _sqlTypeMap.Add("char",         SqlDbType.Char);
                _sqlTypeMap.Add("varchar",      SqlDbType.VarChar);
                _sqlTypeMap.Add("nchar",        SqlDbType.NChar);
                _sqlTypeMap.Add("nvarchar",     SqlDbType.NVarChar);
                //add more here if SqlTypeMap[...] throws an exception
            }
        }
    }
#endif

The magic here lies in the fact that SQL knows all about its sprocs and lets us derive the parameter info. Also nifty here is the use of lookup tables - or in C#, DictionaryBase-derived collections - that lets us cache that info. So, when we ExecSProc, we get _sprocs[procName] which looks up the sproc name in our SProcList. If it's the first time calling this sproc, we NewSProc and add it to the lookup table. From there we just fill and go.

Thanks go to Ian Barker for pointing out the improved syntax via the params keyword. Barry Solomon provided ExecSProcDS, which is handy for returning datasets where your SELECT statement is in an sproc. This routine could be easily modified to return a DataRowCollection like QueryRows, which is a favorite of mine. My queries don't rely on user input (thus not vulnerable to sql injection attack - thanks Curt Drake for the heads up), so I have my SELECT statements in my C# code, and use QueryRows as follows:

C#
string qryStr = "SELECT id, name FROM ...";
foreach (DataRow dr in _sqlConn.QueryRows(qryStr)) { ... }
C#
string qryStr = "SELECT count(*) FROM ...";
if ((int)_sqlConn.QueryRows(qryStr)[0][0] == ...

A couple of readers have pointed out that all of this looks a whole lot like MS Application Data Block. I haven't tried it, and don't know exactly what is involved in getting it installed and working. Perhaps one of you will provide some info. *_*

Hope this helps!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)