brady gaster

yadnb

MT : Problem #3: The Generic DAL: The DalFactory and Provision

Up to now, I've shown you the beginnings of some pretty simple interface implementation, which will yield a relatively flexible model for database access in application development lifecycles. As some of the comments have intelligently pointed out, there's some room for improvement; some exception handling would be nice, a little more flexibility inside the methods would most likely augment the model too. We'll leave that for later, or for reader input (yes, that's a hint - I'd love to see some enhancement on this stuff).

Now, we'll create a factory class that will be used to actually hand out the DAL implementations as needed. This factory will consist solely of static classes, containing multiple overloads of the CreateDbLayer() method. The first of these overloads is relatively simple. Calling code simply says toe the factory “give me a DAL for this type of database” and the DAL does the rest of the work for us.

using System;
namespace GenericDal
{
 /// <summary>
 /// Summary description for DalFactory.
 /// </summary>
 public class DalFactory
 {
  private DalFactory()
  {
  }
  /// <summary>
  /// Creates an instance of the type of DAL being requested. The provided type of object
  /// must implement the IDbLayer interface. 
  /// </summary>
  /// <param name="dataLayerType">The DAL Provider to be returned.</param>
  /// <returns></returns>
  public static IDbLayer CreateDbLayer(Type dataLayerType)
  {
   object o = Activator.CreateInstance(dataLayerType,false);
   if(o is IDbLayer)
    return (IDbLayer)o;
   else
    throw new Exception(o.GetType().ToString() 
     + " is not an implementer of IDbLayer and therefore cannot be returned from this method.");
  }

In this overload, the factory simply activates an instance of the requested DAL and passes it back to the caller. Then, the caller can set the connection string, query the database for DataSet, read resultsets, whatever is needed. We'll add a second overload to this method now, but this time we'll go ahead and set up the connection string property too (may as well kill two birds with one stone, right?).

   /// <summary>
  /// Creates an instance of the type of DAL being requested. The provided type of object
  /// must implement the IDbLayer interface. Also sets the connection string for the provided
  /// instance's Connection property. 
  /// </summary>
  /// <param name="dataLayerType">The DAL Provider to be returned.</param>
  /// <param name="connectionString">The connection string to be opened.</param>
  /// <returns></returns>
  public static IDbLayer CreateDbLayer(Type dataLayerType, 
   string connectionString)
  {
   IDbLayer l = CreateDbLayer(dataLayerType);
   l.ConnectionString = connectionString;
   return l;
  }

This time, an instance of the requested IDbLayer implementor is created and the instance's ConnectionString property set. In this way, we've prepared the implementor before ever sending it back to the caller. Now, the caller can begin performing database operations right away.

Taking a Lesson From ODBC
At this point, we've created a nice framework for accessing data. We've gotten around most of the common problems of connecting and executing commands, abstracted the process of working in a disconnected situation, and made the process of executing database commands relatively simplistic (maybe too simplistic for some!). Now, let's take this whole idea one step further and a little more real-world-friendly

Pretty often in the enterprise development industry, we need to connect to more than one database at a time, or during the execution of a particular application. It would be nice if our DAL could accomodate this need. To do so, we'll borrow a practice from ODBC - the practice of accessing data sources by a given name, or key. To begin this discussion, take a look at the application config file for a client application I'll be demonstrating the construction of later on.

<?xml version="1.0" encoding="utf-8" ?>
<
configuration
>
  <configSections
>
    <section name="DataProviders"
      type="GenericDal.GenericDalConfigurationReader,
        GenericDal"
/>
   </configSections
>
   <DataProviders
>
   <DataSource key="pubsSql" type="GenericDal.SqlServerDal" connectionString="Data   Source=(local); User Id=sa; Password=pass4Sql; Initial Catalog=pubs"
/>
   <DataSource key="nwindSql" type="GenericDal.SqlServerDal" connectionString="Data Source=(local); User Id=sa; Password=pass4Sql; Initial Catalog=Northwind"
/>
   <DataSource key="pubsOleDb" type="GenericDal.OleDbDal" connectionString="Provider=SQLOLEDB.1; Data Source=(local); User Id=sa; Password=pass4Sql; Initial Catalog=pubs"
/>
   </DataProviders
>
</
configuration>

In this example, we've instructed the client application to use three data sources - two running on SQL Server, onr running in some OLEDB space (also SQL Server, perhaps a poor example but the point is clear that we're using multiple connection strategies). Each DataSource element contains an attribute, key, which identifies each DataSource from the others aggregated in the configuration file.

The next step is the authoring of a few configuration-related classes. The first of these will perform the requirements of actually reading the XML node from the configuration file.

using System;
using System.Configuration;
using System.Xml;
using cs = System.Configuration.ConfigurationSettings;
namespace GenericDal
{
 public class GenericDalConfigurationReader : IConfigurationSectionHandler
 {
  /// <summary>
  /// Creates an instance of the GenericDal settings object 
  /// from the custom configuration settings.
  /// </summary>
  /// <param name="parent"></param>
  /// <param name="context"></param>
  /// <param name="section"></param>
  /// <returns></returns>
  public object Create(object parent, 
   object context, 
   XmlNode section) 
  {
   GenericDalSettings settings = new GenericDalSettings();
   XmlNodeList lstSources = section.SelectNodes("DataSource");
   
   for(int i=0; i<lstSources.Count; i++)
   {
    XmlNode nodDataSource = lstSources[i];
    string key = nodDataSource.Attributes["key"].InnerText;
    string t = nodDataSource.Attributes["type"].InnerText;
    string connectionString = nodDataSource.Attributes["connectionString"].InnerText;
    settings.Add(key,t,connectionString);
   }
   return settings;
  }
 }

To persist the settings that are read from the configuration file, we'll create a second class, GenericDalSettings, which will collect and store logical pointers to each of these data sources. In this way, any application has the added functionality of asking the GenericDalSettings class for a particular data source, which is then handed back to the calling application.

// <summary>
 /// The settings class, which contains the type name to be loaded.
 /// </summary>
 public class GenericDalSettings
 {
  /// <summary>
  /// Hidden internal class used by the Settings class 
  /// to differentiate between providers as requested. 
  /// </summary>
  struct DalSetting
  {
   public DalSetting(string key, string connectionString, string iDbProvider)
   {
    Key = key;
    ConnectionString = connectionString;
    IDbProviderType = iDbProvider;
   }
   public string Key;
   public string ConnectionString;
   public string IDbProviderType;
  }
  GenericDalSettings.DalSetting[] sources;
  const string section = "DataProviders";
  /// <summary>
  /// Hide construction logic from external resources. 
  /// </summary>
  internal GenericDalSettings()
  {
   sources = new DalSetting[0];
  }
  /// <summary>
  /// Allows addition of data sources to the internal array.
  /// </summary>
  /// <param name="key">How calling code will identify each IDbLayer
  /// implementation from others</param>
  /// <param name="type">The specific type of IDbLayer implementor
  /// to be used.</param>
  /// <param name="connectionString">Connection string to 
  /// the requested data source.</param>
  internal void Add(string key, string type, string connectionString)
  {
   DalSetting[] tmpSrc = new DalSetting[sources.Length+1];
   Array.Copy(sources,0,tmpSrc,0,sources.Length);
   tmpSrc[sources.Length] = new DalSetting(key,connectionString,type);
   sources = tmpSrc;
  }
  /// <summary>
  /// Returns a particular DataSource by its name. 
  /// </summary>
  public IDbLayer this[string dataSourceName]
  {
   get
   {
    for(int i=0; i<sources.Length; i++)
    {
     if(sources[i].Key.ToLower() == dataSourceName.ToLower())
     {
      return DalFactory.CreateDbLayer(Type.GetType(sources[i].IDbProviderType),
       sources[i].ConnectionString);
     }
    }
    return null;
   }
  }
  /// <summary>
  /// Loads in the data sources and 
  /// prepares the settings class for usage.
  /// </summary>
  /// <returns></returns>
  public static GenericDalSettings GetSettings() 
  {
   GenericDalSettings b = 
    (GenericDalSettings)cs.GetConfig(section);
   return b;
  }
 }
}

Finally (and maybe obviously), we'll add a third overload to the factory class. This overload is relatively simple - it takes a single string parameter that will be used to search for a particular data source by it key.

/// <summary>
  /// Creates an instance of an implementor for the IDbLayer
  /// interface by reading from the custom configuration settings.
  /// See the included sample web config file for an example.
  /// </summary>
  /// <returns></returns>
  public static IDbLayer CreateDbLayer(string dataSourceName)
  {
   string selectedProvider = String.Empty;
   string cnStr = String.Empty;
   
   GenericDalSettings settings = 
    GenericDalSettings.GetSettings();
   
   // figure out the provider
   IDbLayer idb = settings[dataSourceName];
   return idb;
  }

Now that the data layer has been pretty much completely abstracted, applications making use of it have the capability of asking for a particular data source by it's name, then simply calling command routines provided via the IDbLayer's interface requirements. This mimics ODBC in one way - by allowing named requests for data sources. Think of it like this - any application using this data layer has a System-DSN-like methodology for connecting to and executing login within any of the databases required by that application.

As the application grows and requires the need for additional data sources, one need only place new lines of code within the configuration file to access those new data sources (and write code to use them of course).

The next step will take this flexibility idea one step further, to the application logic layer. I'll delve into the workflow conceptualization next, and provide an idea or two for allowing redirection and flexibility within application process flow.

Comments

TrackBack said:

# March 16, 2004 2:28 PM

Greg Robinson said:

Brady, not including exception handling in his code....No?
# March 16, 2004 4:36 PM

brady gaster said:

Nope - read the second post in the series, where i specifically mention the notion of leaving the exception handling to the uppermost client layer. Trust me, it'll all make sense soon.
# March 16, 2004 5:04 PM

Joel Ross said:

Have you looked at the MS Data Application Building Blocks (3.0 more specifically)? How does your implementation differ from theirs?

Their's looks very similar, except rather than an interface, they use a abstract base class that you inherit from, which has the majority of the functionality in it, written once.
# March 17, 2004 10:23 AM

brady gaster said:

I actually had not seen this implementation, but you can bet i'll be taking a look-see at it. I'll be moving forward with the rest of this architectural idea i've come up with in the days to come, so keep tabs and let me know what you think of the rest of the ideas presented here.
# March 17, 2004 10:31 AM
Leave a Comment

(required) 

(required) 

(optional)

(required)