Tom’s Blog

February 21, 2009

Exploring SQL Server Schema Information With ADO.NET

Filed under: ADO.NET,C# — Tom Shelton @ 7:00 pm

If you read my article on finding SQL Server instances, you’ll know that I mentioned that I planned on writing a series of articles around a code generator that I have been playing with off and on for the last couple of years.  Since this generator was designed to create class definitions around tables, views, and procedures in an SQL Server database, I needed to figure out and write the code to get schema information from a given connection.  As it turns out, ADO.NET provides a fairly robust mechanism for implementers of data providers to return this information from the underlying data store.  In this article, I will be focusing on the information available from the System.Data.SqlClient namespace – but, techniques shown here apply to all of the data providers built into the .NET framework.  I have similar code using System.Data.OracleClient that I can provide if it is of interest.

I’ll start off simple and create a small C# console application that can list all of the databases in an SQL Server instance.

 

   1: using System;
   2: using System.Data;
   3: using System.Data.SqlClient;
   4:  
   5: namespace SchemaInfo
   6: {
   7:     class Program
   8:     {
   9:         static void Main ( string[] args )
  10:         {
  11:             // build a connection string to a sql server instance
  12:             SqlConnectionStringBuilder connectionBuilder = new SqlConnectionStringBuilder ();
  13:             connectionBuilder.DataSource = "SAURON";
  14:             connectionBuilder.IntegratedSecurity = true;
  15:  
  16:             // now, lets list all of the databases in this instance
  17:             using ( SqlConnection connection = new SqlConnection ( connectionBuilder.ConnectionString ) )
  18:             {
  19:                 connection.Open ();
  20:  
  21:                 // get the database information
  22:                 DataTable databases = connection.GetSchema ( SqlClientMetaDataCollectionNames.Databases );
  23:  
  24:                 // print out the connections
  25:                 foreach ( DataColumn column in databases.Columns )
  26:                 {
  27:                     Console.Write ( "{0,-25}", column.ColumnName );
  28:                 }
  29:                 Console.WriteLine ();
  30:                 
  31:                 // print out the rows...
  32:                 foreach ( DataRow database in databases.Rows )
  33:                 {
  34:                     for ( int i = 0; i < database.ItemArray.Length; i++ )
  35:                     {
  36:                         Console.Write ( "{0,-25}", database.ItemArray[i] );
  37:                     }
  38:                     Console.WriteLine ();
  39:                 }
  40:             }
  41:         }
  42:     }
  43: }

schema001

The key to the above codes success is the SqlConnection.GetSchema method.  By passing in a string that represents the name of one of the provider’s supported schema collections, you can get a DataTable back containing the requested information.  There are essentially two types of schema collections – Common Schema Collections and Provider Specific Schema Collections.   As the names imply, the common schema collections are the schema collections implemented by all of the .NET framework providers, where as the provider specific collections apply to a specific provider.

You can get a list of the schema collections supported by your provider by calling the GetSchema method with no arguments.  Here is the above example, with the schema collection name commented out:

 

   1: using System;
   2: using System.Data;
   3: using System.Data.SqlClient;
   4: using System.Windows.Forms;
   5:  
   6: namespace SchemaInfo
   7: {
   8:     class Program
   9:     {
  10:         static void Main ( string[] args )
  11:         {
  12:             // build a connection string to a sql server instance
  13:             SqlConnectionStringBuilder connectionBuilder = new SqlConnectionStringBuilder ();
  14:             connectionBuilder.DataSource = "SAURON";
  15:             connectionBuilder.IntegratedSecurity = true;
  16:  
  17:             // now, lets list all of the databases in this instance
  18:             using ( SqlConnection connection = new SqlConnection ( connectionBuilder.ConnectionString ) )
  19:             {
  20:                 connection.Open ();
  21:  
  22:                 // get the database information
  23:                 DataTable databases = connection.GetSchema ( /*SqlClientMetaDataCollectionNames.Databases*/ );
  24:  
  25:                 // print out the connections
  26:                 foreach ( DataColumn column in databases.Columns )
  27:                 {
  28:                     Console.Write ( "{0,-25}", column.ColumnName );
  29:                 }
  30:                 Console.WriteLine ();
  31:  
  32:                 // print out the rows...
  33:                 foreach ( DataRow database in databases.Rows )
  34:                 {
  35:                     for ( int i = 0; i < database.ItemArray.Length; i++ )
  36:                     {
  37:                         Console.Write ( "{0,-25}", database.ItemArray[i] );
  38:                     }
  39:                     Console.WriteLine ();
  40:                 }
  41:             }
  42:         }
  43:     }
  44: }

schema002

You can find a lot of information on using the GetSchema method and the various schema collections here.

So, now that we’ve introduced the concept GetSchema and schema collections, let’s do something a little more complex.  Let’s build a database explorer type control.  Something like a very simplified version of the Object Explorer in SQL Server Management Studio.

 

1.

Start Visual Studio, and start a new blank solution.  Do this by selecting File -> New -> Project and then selecting "Other Project Types -> Visual Studio Solutions" in the "Project Types" tree view in the "New Project" dialog.

  schema003
   

2.

Add a Windows Forms application to the blank project to serve as a test harness for the control.  Right click on the solution in "Solution Explorer" and select "Add -> New Project…".

  schema004
   

3.

Now, following the same steps as before, add a new "Windows Forms Control Library" to the solution.

  schema005
   

4.

In the windows forms project add a reference to the control library project.  To do this, right click on the windows form project in the "Solution Explorer" and select "Add Reference…".  Select the control library on the Projects tab of the "Add Reference" dialog.  Select OK.

  schema006
   

5.

Rename the default UserControl1 in the windows control library to something more appropriate.  The easiest way to do this is simply right click on the control in the "Solution Explorer" and select "Rename".  I called mine "DBExplorer".  Compile the solution.

   

6.

Now, we create a simple interface so we can see the control in action.  I added a SplitContainer to the default Form1 in the windows forms application.  Then added an instance of the DBExplorer control to Panel1 of the SplitContainer, and set it’s Dock property to Fill.  I named the DBExplorer instance uxDBExplorer.

  schema007
 

Not much to look at – but, this is just a test harness :)

   

7.

Go back to the control library project and add a treeview control to the DBExplorer interface.  Set it’s Dock property to fill and give it an appropriate name.  I gave mine the name of uxExplorer.  Compile the solution.

   

8.

Here is the code for the DBExplorer user control:

 
   1: using System;
   2: using System.Data;
   3: using System.Data.SqlClient;
   4: using System.Windows.Forms;
   5:  
   6: namespace DatabaseExplorer
   7: {
   8:     public partial class DBExplorer : UserControl
   9:     {
  10:         public DBExplorer ()
  11:         {
  12:             InitializeComponent ();
  13:         }
  14:  
  15:         public string ConnectionString { get; set; }
  16:  
  17:         private void DBExplorer_Load ( object sender, EventArgs e )
  18:         {
  19:             if ( !this.DesignMode && !string.IsNullOrEmpty ( this.ConnectionString ) )
  20:             {
  21:                 // create the first node...
  22:                 this.uxExplorer.Nodes.Add ( new ConnectionNode ( new SqlConnectionStringBuilder ( this.ConnectionString ) ) );
  23:             }
  24:         }
  25:  
  26:         private void uxExplorer_BeforeExpand ( object sender, TreeViewCancelEventArgs e )
  27:         {
  28:             if ( e.Node is DataSourceNodeBase )
  29:             {
  30:                 string oldText = e.Node.Text;
  31:                 try
  32:                 {
  33:                     e.Node.Text = string.Format ( "{0} (expanding...)", e.Node.Text );
  34:                     this.uxExplorer.Refresh ();
  35:                     this.uxExplorer.BeginUpdate ();
  36:                     ( (DataSourceNodeBase)e.Node ).Load ();
  37:                 }
  38:                 catch ( Exception ex )
  39:                 {
  40:                     MessageBox.Show ( ex.Message );
  41:                 }
  42:                 finally
  43:                 {
  44:                     this.uxExplorer.EndUpdate ();
  45:                     e.Node.Text = oldText;
  46:                 }
  47:             }
  48:         }
  49:  
  50:         #region Private Classes
  51:         /// <summary>
  52:         /// Used as a simple placeholder before an expand...
  53:         /// </summary>
  54:         private class DummyNode : TreeNode
  55:         {
  56:             public DummyNode ()
  57:                 : base ( "DUMMY" )
  58:             {
  59:                 this.Name = "DUMMY";
  60:             }
  61:         }
  62:  
  63:         /// <summary>
  64:         /// Provides a base class for a node that must connect to a
  65:         /// datasource.
  66:         /// </summary>
  67:         private abstract class DataSourceNodeBase : TreeNode
  68:         {
  69:             protected DataSourceNodeBase ( string text, SqlConnectionStringBuilder builder )
  70:                 : base ( text )
  71:             {
  72:                 this.ConnectionStringBuilder = builder;
  73:                 this.Nodes.Add ( new DummyNode () );
  74:             }
  75:             
  76:             /// <summary>
  77:             /// The connection string used to connect to the datasource
  78:             /// </summary>
  79:             protected SqlConnectionStringBuilder ConnectionStringBuilder { get; set; }
  80:  
  81:             /// <summary>
  82:             /// Loads child nodes if it hasn't been loaded before
  83:             /// </summary>
  84:             public virtual void Load ()
  85:             {
  86:                 if ( this.Nodes.ContainsKey ( "DUMMY" ) )
  87:                 {
  88:                     this.Reload ();
  89:                 }
  90:             }
  91:  
  92:             /// <summary>
  93:             /// Refreshes children from the datasource
  94:             /// </summary>
  95:             public virtual void Reload ()
  96:             {
  97:                 this.Nodes.Clear ();
  98:                 this.LoadDatabaseObjects ();
  99:             }
 100:  
 101:             /// <summary>
 102:             /// Loads subnodes from the datasource
 103:             /// </summary>
 104:             protected abstract void LoadDatabaseObjects ();
 105:         }
 106:  
 107:         /// <summary>
 108:         /// Root node for a datasource connection
 109:         /// </summary>
 110:         private class ConnectionNode : DataSourceNodeBase
 111:         {
 112:             public ConnectionNode ( SqlConnectionStringBuilder builder ) : base ( builder.DataSource, builder ) { }
 113:  
 114:             protected override void LoadDatabaseObjects ()
 115:             {
 116:                 using ( SqlConnection connection = new SqlConnection ( this.ConnectionStringBuilder.ConnectionString ) )
 117:                 {
 118:                     connection.Open ();
 119:                     DataTable databases = connection.GetSchema ( SqlClientMetaDataCollectionNames.Databases );
 120:                     foreach ( DataRow database in databases.Rows )
 121:                     {
 122:                         SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder ( this.ConnectionStringBuilder.ConnectionString );
 123:                         builder.InitialCatalog = (string)database["database_name"];
 124:                         this.Nodes.Add ( new DatabaseNode ( builder ) );
 125:                     }
 126:                 }
 127:             }
 128:         }
 129:  
 130:         /// <summary>   
 131:         /// Node whose text property contains a database.
 132:         /// </summary>
 133:         private class DatabaseNode : TreeNode
 134:         {
 135:             public DatabaseNode ( SqlConnectionStringBuilder builder )
 136:                 : base ( builder.InitialCatalog )
 137:             {
 138:                 this.Nodes.Add ( new TableRootNode ( builder ) );
 139:                 this.Nodes.Add ( new ViewRootNode ( builder ) );
 140:                 this.Nodes.Add ( new StoredProcedureRootNode ( builder ) );
 141:             }
 142:         }
 143:  
 144:         /// <summary>
 145:         /// Root node for all base tables
 146:         /// </summary>
 147:         private class TableRootNode : DataSourceNodeBase
 148:         {
 149:             public TableRootNode ( SqlConnectionStringBuilder builder ) : base ( "Tables", builder ) { }
 150:  
 151:             protected override void LoadDatabaseObjects ()
 152:             {
 153:                 using ( SqlConnection connection = new SqlConnection ( this.ConnectionStringBuilder.ConnectionString ) )
 154:                 {
 155:                     connection.Open ();
 156:                     DataTable tables = connection.GetSchema ( SqlClientMetaDataCollectionNames.Tables, new string[] { null, null, null, "BASE TABLE" } );
 157:                     foreach ( DataRow table in tables.Rows )
 158:                     {
 159:                         this.Nodes.Add ( new TableNode ( string.Format ( "{0}.{1}", table["TABLE_SCHEMA"], table["TABLE_NAME"] ), this.ConnectionStringBuilder ) );
 160:                     }
 161:                 }
 162:             }
 163:         }
 164:  
 165:         /// <summary>
 166:         /// Root node for all views
 167:         /// </summary>
 168:         private class ViewRootNode : DataSourceNodeBase
 169:         {
 170:             public ViewRootNode ( SqlConnectionStringBuilder builder ) : base ( "Views", builder ) { }
 171:  
 172:             protected override void LoadDatabaseObjects ()
 173:             {
 174:                 using ( SqlConnection connection = new SqlConnection ( this.ConnectionStringBuilder.ConnectionString ) )
 175:                 {
 176:                     connection.Open ();
 177:                     DataTable tables = connection.GetSchema ( SqlClientMetaDataCollectionNames.Tables, new string[] { null, null, null, "VIEW" } );
 178:                     foreach ( DataRow table in tables.Rows )
 179:                     {
 180:                         this.Nodes.Add ( new TableNode ( string.Format ( "{0}.{1}", table["TABLE_SCHEMA"], table["TABLE_NAME"] ), this.ConnectionStringBuilder ) );
 181:                     }
 182:                 }
 183:             }
 184:         }
 185:  
 186:         /// <summary>
 187:         /// Node to store information about tables and views.
 188:         /// </summary>
 189:         private class TableNode : DataSourceNodeBase
 190:         {
 191:             public TableNode ( string text, SqlConnectionStringBuilder builder )
 192:                 : base ( text, builder )
 193:             {
 194:                 
 195:             }
 196:  
 197:             protected override void LoadDatabaseObjects ()
 198:             {
 199:                 // TODO: GATHER FIELD INFORMATION
 200:             }
 201:  
 202:         }
 203:  
 204:         /// <summary>
 205:         /// Node that when expanding, will get stored procedure information
 206:         /// </summary>
 207:         private class StoredProcedureRootNode : DataSourceNodeBase
 208:         {
 209:             public StoredProcedureRootNode ( SqlConnectionStringBuilder builder ) : base ( "Stored Procedures", builder ) { }
 210:  
 211:             protected override void LoadDatabaseObjects ()
 212:             {
 213:                 using ( SqlConnection connection = new SqlConnection ( this.ConnectionStringBuilder.ConnectionString ) )
 214:                 {
 215:                     connection.Open ();
 216:                     DataTable procedures = connection.GetSchema ( SqlClientMetaDataCollectionNames.Procedures, new string[] { null, null, null, "PROCEDURE" } );
 217:                     foreach ( DataRow procedure in procedures.Rows )
 218:                     {
 219:  
 220:                         this.Nodes.Add ( new ProcedureNode ( string.Format ( "{0}.{1}", procedure["SPECIFIC_SCHEMA"], procedure["SPECIFIC_NAME"] ), this.ConnectionStringBuilder ) );
 221:                     }
 222:                 }
 223:             }
 224:         }
 225:  
 226:         /// <summary>
 227:         /// Node to store information about a stored procedures
 228:         /// </summary>
 229:         private class ProcedureNode : DataSourceNodeBase
 230:         {
 231:             public ProcedureNode ( string text, SqlConnectionStringBuilder builder )
 232:                 : base ( text, builder )
 233:             {
 234:             }
 235:  
 236:             protected override void LoadDatabaseObjects ()
 237:             {
 238:                 // TODO: GATHER PARAMETER INFORMATION...
 239:             }
 240:         }
 241:         #endregion
 242:     }
 243: }

   

9.

In the test forms designer, set the ConnectionString property of the DBExplorer instance to point to a SQL Server Instance.  Here is the result when I point it to a local instance, and run the project:

  schema008

I purposefully left the implementation of gathering field and parameter info to the reader – but, I hope that this was enough to introduce the key concepts of using the GetSchema method of the SqlConnection class to dynamically discover schema infromation.  In the entity generator that I have been discussing, I use a variant of this exact control.  The major differences being that it supports multiple connection nodes and displays images for the various database objects.

Powered by WordPress