Click here to Skip to main content
Click here to Skip to main content

Don't hard code your DataProviders

By , 1 Feb 2010
 

Introduction

I am tired of seeing people writing their own data access layers and wrappers to enable "simplified" code. The thing is, they rarely actually simplify code. Instead, I see new configuration sections, whole configuration files, and entirely new object structures to somehow make things better when .NET framework offers most of the functionality I see written over and over again.

So, this article is to make things more clear, and truly simplified.

Bits

Configuration file

In the App.Config or Web.Config, there is an available section called connectionStrings. Inside that is add elements, these are individual connections to databases. Each add element has three important bits: name,connectionString, and providerName.

  • name is obvious, this is the name of the connection.
  • connectionString is also obvious, this is the connection string.
  • providerName is not obvious, and everyone seems to miss it. This allows us to dynamically create the objects we need, using any properly installed DataProvider. I will come back to this in a moment.

Code bits

Starting with .NET 2.0, all .NET DataProviders inherit from a certain set of classes found in System.Data.Common. All of these can be created dynamically from the provider factory. The following table is a quick cross reference between the objects that are more familiar with the base classes that we need to reference.

Base object SqlClient object OleDb Object
DbConnection SqlConnection OleDbConnection
DbCommand SqlCommand OleDbCommand
DbParameter SqlParameter OleDbParameter
DbConnectionStringBuilder SqlConnectionStringBuilder OleDbConnection StringBuilder
DbProviderFactory SqlClientFactory OleDbFactory

There are other classes, but these are the ones I am going to touch on. Let's get started.

Using the bits

Configuration

Here is a simple App.Config with example connections for most popular databases: SQL Server, Oracle, PostgresSQL, MySQL, and MS Access.

<?xml version="1.0"?>
<configuration>
  <appSettings>
    <!-- all your application settings -->
  </appSettings>
  <connectionStrings>
    <add name="blah" providerName="System.Data.SqlClient" 
      connectionString="Data Source=(local);Initial Catalog=cars;Integrated Security=SSPI"/>
    <!-- examples of other providers OleDb, Oracle (using TNS.ORA), 
            MySQL and Npgsql with sample connectionstrings from connectionstrings.com -->
    <!--    <add name="blah" providerName="Npgsql" 
      connectionString="Server=127.0.0.1;Port=5432;Database=myDataBase;
        User Id=myUsername;Password=myPassword;"/> -->
    <!--    <add name="blah" providerName="System.Data.OleDb" 
      connectionString="Provider=Microsoft.Jet.OLEDB.4.0;
        Data Source=C:\mydatabase.mdb;User Id=admin;Password=;"/> -->
    <!--    <add name="blah" providerName="MySql.Data.MySqlClient" 
      connectionString="User ID=root;Password=myPassword;Host=localhost;Port=3306;
         Database=myDataBase; Direct=true;Protocol=TCP;Compress=false;Pooling=true;
         Min Pool Size=0;Max Pool Size=100;Connection Lifetime=0;"/> -->
    <!--    <add name="blah" providerName="Oracle.DataAccess.Client" 
      connectionString="Data Source=TORCL;User Id=myUsername;Password=myPassword;"/> -->
  </connectionStrings>
</configuration>

As you can see, by changing the connection string, we connect to different databases. Nothing new here. We could do that with OLEDB and ODBC, but with both those technologies, we are passing through layers of code. With the managed providers, chances are they are 100% managed code, which means no layers to go through, thus much faster access. Additionally, by using the managed provider, we will get the greatest performance and flexibility (like named parameters) from our database.

The one thing to note here is that some of these providers are third party providers, and must be installed. If you use them, you may have to do some of the install yourself.

Important not: Do not use the Microsoft Oracle Provider (System.Data.OracleClient), it is being eliminated. Although, if you use the information in this article, it will only require a change to the configuration file.

Make the connection

Now that we have a configuration we can use, let's create a connection to the configured database.

//get the information out of the configuration file.
ConnectionStringSettings connectionStringSettings = 
       ConfigurationManager.ConnectionStrings["blah"];

//get the proper factory 
DbProviderFactory factory = 
  DbProviderFactories.GetFactory(connectionStringSettings.ProviderName);

//create a command of the proper type.
DbConnection conn = factory.CreateConnection();
//set the connection string
conn.ConnectionString = connectionStringSettings.ConnectionString;

//open the connection
conn.Open();

Now I can connect to any database with the proper provider and connection string.

Modifying and verifying the connection string

Let's go back to that code for a second. Yes, it works, but what about making sure that certain features are turned on? For this example, I am going to pick on SQL Server, and a feature I love: MultipleActiveRecordSets.

//get the information out of the configuration file.
ConnectionStringSettings connectionStringSettings = 
   ConfigurationManager.ConnectionStrings["blah"];

//get the proper factory 
DbProviderFactory factory = 
  DbProviderFactories.GetFactory(connectionStringSettings.ProviderName);

string connectionString = connectionStringSettings.ConnectionString;

DbConnectionStringBuilder csb = factory.CreateConnectionStringBuilder();
//make sure it got created
if (csb != null)
{
    //let the ConnectionStringBuilder parse the current connection string
    csb.ConnectionString = connectionString;
    //if this provider supports this setting, set it.
    if (csb.ContainsKey("MultipleActiveResultSets"))
    {
        csb["MultipleActiveResultSets"] = true;
    }
    //use the new modified connection string
    connectionString = csb.ConnectionString;
}

//create a command of the proper type.
DbConnection conn = factory.CreateConnection();
//set the connection string
conn.ConnectionString = connectionString;

//open the connection
conn.Open();

Now, if my connection supports it, MultipleActiveRecordSets will be set to true. This works regardless of which provider we might be using. Unfortunately, some providers don't support the keys correctly, and some providers like the "Microsoft SQL Server Compact Data Provider" don't provide the connection string builder object at all.

Executing a query

For the sake of clarity, I am going to simply continue with the function above.

//by using the CreateCommand function on the connection rather than 
//using factory.CreateCommand()
//the connection is automatically associated with the new command
DbCommand cmd = conn.CreateCommand();
cmd.CommandText = "select * from Vehicle";

//execute a datareader, closing the connection when all the data is read from it
using(DbDataReader dr = cmd.ExecuteReader(CommandBehavior.CloseConnection))
{
    while(dr.Read())
    {
        //do something with the data
    }
}

Creating/Using parameters

When we create a parameter with factory.CreateParameter(), we get a DbParameter object. Well, that is fine, but it will throw some people for a loop. You see, you no longer get access to the provider specific DbType, and even though there are tricks we could use to go get the right settings, it is of limited help because each provider uses a differently named property to set their own enum. Instead, what we need to do is use the DbType that is defined in the base class, like this:

//create a 16 byte random "salt" to put into the database 
byte[] b = new byte[16];
RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
rng.GetBytes(b);

//createa parameter and fill it
p = factory.CreateParameter();
p.DbType = DbType.Binary;
p.Value = b;
p.ParameterName = "salt";

Getting all installed data providers

Here is a simple line of code to see which providers are installed, in case you want to dynamically create a connection by letting the user input the connection info, or perhaps double checking the configured provider is actually installed:

DataTable dt = System.Data.Common.DbProviderFactories.GetFactoryClasses() 

One last trick

I see lots of toolkits, articles, and tricks about things like writing Insert and Update statements, but .NET provides you with that functionality too; here is the code for that. Taking advantage of the DbCommandBuilder object, connected DataTables create their SQL for Insert, Update, and Deletes using these objects.

using (DbCommand cmd = conn.CreateCommand())
{
    cmd.CommandType = CommandType.Text;
    cmd.CommandText = "select * from " + tableName;
    cmd.CommandTimeout = 10;
    using (DbDataAdapter da = factory.CreateDataAdapter())
    {
        DbCommandBuilder cb = factory.CreateCommandBuilder();
        da.SelectCommand = cmd;
        DataTable dt = new DataTable();
        da.FillSchema(dt, SchemaType.Source);
        cb.DataAdapter = da;
        DbCommand[] cmds = new DbCommand[3]
        cmds[0] = cb.GetUpdateCommand();
        cmds[1] = cb.GetDeleteCommand();
        cmds[2] = cb.GetInsertCommand();
    }
}

Points of interest

Correctly using the Factory pattern in this way prevents hard-coding of your provider. Even better, if you are using a third party provider like Oracle, MySQL, or Npgsql, if we don't directly reference that assembly (DLL), we can upgrade the provider without it breaking our code.

In the machine.config is a connection string that is called "LocalSqlServer"; the default provider (as expected) is "System.Data.SqlClient", and the connection string is "data source=.\SQLEXPRESS;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|aspnetdb.mdf;User Instance=true", which is great for many web applications. In truth, there is no reason you couldn't use that connection string for Windows applications as well. This extra connection string, as well as the others you inherit (especially under web applications), is something to be aware of before trying to do something like:

foreach(var cs in ConfigurationManager.ConnectionStrings)
{
   //create and test each connection
}

Obviously, this is not the only bit of data you need to make your code database independent, but it is the first thing. Some of the other bits can be seen in some of my other articles here, here, and here.

If you need help making connection strings, http://connectionstrings.com is a great resource.

History

None so far.

License

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

About the Author

Alaric Dailey
Software Developer (Senior) Pengdows
United States United States
Member
Currently looking for new contracts in Omaha NE or telecommute opportunities.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralNot going to work completelymemberFrans Bouma1 Feb '10 - 21:51 
Although the message to use as much generic code as possible is OK, there are some problems with this api. The DbProviderFactory is nice for creating objects, but you run into problems when inserting or updating data, as you then need to create parameters, and the types to specify can't always use DbType, as for example in the SqlClient and also in Oracle ODP.NET there are edge cases for some types: using a DbType type will choose the wrong ADO.NET provider specific type. For example to get the parameter to be set to the SqlDbType.Time or SqlDbType.Date (SqlServer 2008+ specific types), you can't use any DbType type. Using the value also doesn't work, as the value is in a datetime.
 
Also, when an exception occurs, the generic exception is actually unusable: the specific information provided by the provider requires specific code.
 
So although hardcoding a lot of code isn't necessary anymore, one still has to program code to obtain and set the proper types for the parameters and handle the exceptions. This makes using dbproviderfactory oriented code still worth it, but it's not as great as one might think. Actually, the api clearly looks like it's been designed in a clean room without looking at actually real-world scenarios.
 
--
Lead developer of LLBLGen Pro: http://www.llblgen.com
Only the true wise understand the difference between knowledge and wisdom.

GeneralRe: Not going to work completely (mapping types back and forth)memberAlaric Dailey2 Feb '10 - 2:39 
Frans Bouma wrote:
Although the message to use as much generic code as possible is OK, there are some problems with this api. The DbProviderFactory is nice for creating objects, but you run into problems when inserting or updating data, as you then need to create parameters, and the types to specify can't always use DbType, as for example in the SqlClient and also in Oracle ODP.NET there are edge cases for some types: using a DbType type will choose the wrong ADO.NET provider specific type. For example to get the parameter to be set to the SqlDbType.Time or SqlDbType.Date (SqlServer 2008+ specific types), you can't use any DbType type. Using the value also doesn't work, as the value is in a datetime.

 
This depends on the underlying column type, if they columns are different, truly different, say having a currency type in SQL Server and having to resort to a Numeric(16,4) to get the same precision in Oracle, TimesTen and PostgreSQL, then TRUE it won't work perfectly.
 
On the otherhand you can do this in a test application to find the mappings back and forth. (Obviously swapping out your provider)
 
SqlParameter sqlParameter = new SqlParameter();
sqlParameter.SqlDbType = SqlDbType.Money; //money doesn't exist in the DbType enumeration
Debug.WriteLine(Enum.GetName(typeof(DbType), sqlParameter.DbType));
 
Also the DbProvider must support the DataTypes MetaDatacollection, and we can map all provider specific types back to their Underlying DbType with the following code.
//get our provider factory and connection
    DbProviderFactory factory = DbProviderFactories.GetFactory("System.Data.SqlClient");
    DbConnection connection = factory.CreateConnection();
    connection.ConnectionString = "Data Source=(local);Integrated Security=SSPI;Initial Catalog=cars;";
//connect
    connection.Open();
//create a parameter to let it do the mapping for us
    DbParameter providerParameter = factory.CreateParameter();
 
//get the type for reflecting
    Type parameterType = providerParameter.GetType();
//find the provider specific DbType property
    PropertyInfo[] pis =
        parameterType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
    PropertyInfo providerDbTypeProperty = null;
    foreach (PropertyInfo pi in pis)
    {
//ignore the "DbType" property, instead, get the other one
        if (pi.Name.IndexOf("DbType") > 0)
        {
            providerDbTypeProperty = pi;
            break;
        }
    }
    if (providerDbTypeProperty == null)
    {
        throw new Exception("couldn't find providers native DbType");
    }
 
//get our metadata collection
    using (DataTable dt = connection.GetSchema(DbMetaDataCollectionNames.DataTypes))
    {
        DbParameter parameter = providerParameter;
//use the column that provides the number for the value
        DataColumn column = dt.Columns[DbMetaDataColumnNames.ProviderDbType];
 
//create mapping dictionary
        Dictionary<string, DbType> dictionary = new Dictionary<string, DbType>();
        foreach (DataRow row in dt.Rows)
        {
//set a default
            parameter.DbType = DbType.Object;
//get value
            object value = row[column];
//set the property via reflection
            providerDbTypeProperty.SetValue(providerParameter, value, null);
//get the NAME that the provider specifies
            string name = Enum.GetName(providerDbTypeProperty.PropertyType, value);
            try
            {
//add that Name, and mapping over to my dictionary
                dictionary.Add(name, parameter.DbType);
            }
            catch
            {
                //eat the errors of the duplicates;
            }
        }
//put a breakpoint here to inspect the dictionary
        Debug.WriteLine("stop");
//return it if we want
        return dictionary;
    }

GeneralRe: Not going to work completely (mapping types back and forth)memberAlaric Dailey2 Feb '10 - 3:22 
Well, apparently not all providers support the DataTypes table as I thought, so I rewrote the code to work without it.
 
    DbProviderFactory factory = DbProviderFactories.GetFactory(providerName);
    DbParameter providerParameter = factory.CreateParameter();
    Type parameterType = providerParameter.GetType();
    PropertyInfo[] pis =
        parameterType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
    PropertyInfo providerDbTypeProperty = null;
    foreach (PropertyInfo pi in pis)
    {
        if (pi.Name.IndexOf("DbType") > 0)
        {
            providerDbTypeProperty = pi;
            break;
        }
    }
    if (providerDbTypeProperty == null)
    {
        throw new Exception("couldn't find providers native DbType");
    }
 
    {
        DbParameter parameter = providerParameter;
        var values = Enum.GetValues(providerDbTypeProperty.PropertyType);
        Dictionary<string, DbType> dictionary = new Dictionary<string, DbType>();
        foreach (var value in values)
        {
            parameter.DbType = DbType.Object;
           
            providerDbTypeProperty.SetValue(providerParameter, value, null);
            string name = Enum.GetName(providerDbTypeProperty.PropertyType, value);
            try
            {
                dictionary.Add(name, parameter.DbType);
            }
            catch
            {
                //eat the errors of the duplicates;
            }
        }
        return dictionary;
    }

GeneralRe: Not going to work completely (mapping types back and forth)memberlu1888710 Oct '12 - 21:44 
such a good article!! thanks a lot for helping me !
GeneralThanksmemberNightJammer1 Feb '10 - 10:07 
I very much liked this article. Well thought out, and very usefull to beginners Smile | :) thanks man.
 
/////////////////
Groucho Marx
Those are my principles, if you don't like them… I have others.

QuestionHow to use parameters?membersupercat91 Feb '10 - 7:05 
In SQL Server, one uses parameters by including their names in an SQL query. In Access, one includes question marks and parameters are taken in order, without regard for name. How should one form the query strings?
 
In my present code, I have a wrapper class which can go around either access or SQL Server. It uses a StringBuilder to assemble query strings, and it has functions for AddStringParam, AddIntParam, etc.; these functions will generate a new variable name, append it to the StringBuilder, and add the parameter to the query object. This makes it practical to send a single query object with a bunch of INSERT commands, since there doesn't seem to be any portable way to do that otherwise, and using a separate query to insert each row would be dog slow.
 
Also, is there any nice portable way to get the server time?
AnswerRe: How to use parameters?memberAlaric Dailey1 Feb '10 - 7:18 
Actually, YES!!!
 
You need to have the DataSourceInformation from the providers MetaData.
 
I have an article on how to do this already!
 
Using Information from the .NET DataProvider
AnswerRe: How to use parameters?memberAlaric Dailey1 Feb '10 - 7:36 
Apparently I didn't read completely.
 
My first answer is correct for the first part of you question.
 
As far as your present code goes a couple of things. Sending multiple commands is possible if your datasourceinformation metadata has something in the "StatementSeparatorPattern" however that may not be helpful if you are using a database that is limited like TimesTen.
 
Using ConnectionPooling is probably a better choice. The last choice is the opposite, wrap everything inside a transaction, by since it will all go at once, sometimes this is faster.
 
As far as Server time goes, unfortunately as portable as you can get is the SQL92 standard
 
select CURRENT_TIMESTAMP 
 
The problem is, lots of databases STILL don't support SQL92, MS Access jumps to mind.
GeneralRe: How to use parameters?memberAlaric Dailey1 Feb '10 - 8:12 
I cracked open the querytool I wrote that uses the info provided in my articles.
 
it appears that access supports
 
select now()
 
as do many other providers, however some (like SQL Server) do not.
 
My suggestion is to extend the information in my DataSourceInformation class, try the SQL92 version first, and if that fails use the now(), and cache that information in a handy spot. That way when you need the servers time, you can get it.
 
The problem is things like Daylight Savings Time, timezones, drift of computer clocks etc. That isn't even taking into account the issue with the fact that the time will have changed by the end of the round trip.
 
The BEST solution, although not necessarily the most feasible, is to set all servers to UTC time (GMT with no daylight savings). Then make sure all of them are being synced with a reliable NTP source. Such as The NTP Pool.
GeneralRe: How to use parameters?membersupercat91 Feb '10 - 11:22 
Alaric Dailey wrote:
It appears that access supports select now() as do many other providers, however some (like SQL Server) do not.

 
Since Access and SQL Server are the two platforms I'm trying to support, the fact that their Now() functions differ is a pain. I set a variable called NowFunct which contains the name of the SQL provider's time-of-day function. A bit irksome, but it seems to work. I was wondering if there was any better way (it doesn't seem so).
 
With regard to using connection pooling for speed, when my program adds log records to the database, it creates one connection, does all the log records, and exits. Things seem much faster when the inserts are grouped into a small number of SqlExecuteNonQuery operations than when they are executed individually. Transactions didn't seem to help much. Using a bulk insert helps things enormously, but is non-portable.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web03 | 2.6.130523.1 | Last Updated 1 Feb 2010
Article Copyright 2010 by Alaric Dailey
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid