March 2004 - Posts
Thanks to Denny, I've received an awesome pointer to
Northwoods, makers of an awesome WinForms (and WebForms) product called GoDiagram that makes developing Visio-like interfaces a SNAP! This is about the most awesome control series I've seen. What's even better is that the samples contain a “Web Walking” program that you can aim at your URL's to find all the other places you're linking to. How awesome! A VERY cool product indeed, worth checking out.
Think Visio. You put two boxes (let's say WinForms Panels, 'cuz that's what I'm using) onto a workspace (yet another Panel). You click one of them and you begin to draw a line. You move your mouse over to another panel and click it. “Click!” There is now a line connecting those two panels. And when you move the panels around on the workspace, the line stays connected to both Panels and gets longer/short depending on how you move the Panels around on the workspace.
Anyone know how to pull this off? Got links? Got samples?
Did anyone get a copy of this!?!?!? But, I just sat down to download it!
Jeff actually has a really good point on turning off IntelliSense for a day. I tried this during some classes I've taught - namely the .Net Framework class, which discusses things like assembly linkage, compiler switches, and that sort of fun stuff. The interesting thing was how the students all ended up relatively amused with their apparent lack of syntax. Even those who had gotten to learn C# tended to be challenged at times. I think its a good challenge, Jeff.
Now if I didn't have all of these deadlines that IntelliSense makes so much easier to reach.
The second problem plagues me. I know it plagues us all. How many times have you heard yourself think - or even say out loud - how was this missed? Why am I retrofitting my code now for this? How in the heck did someone forget this?
This problem is so resident in so many places, projects, and situations. My current project, for instance, had requirement changes days - hours - before deployment. Why? Changes in process, restructuing, that sort of thing. Business changes. The processes which drive it evolve, just like the real world around us. The applications we write should adapt as the world does.
This is the workflow part of the essay. During this section, I'll expose you to the workflow-specific areas within MT, and point out how this idea can limit rework later on. I'll make use of some typical OO concept to demonstrate this stuff, and remind you - the more work you put up front, the less you do in the end.
So let's get started - the first part of the workflow portion of MT is the IProcessStep interface. Here's the code for it:
using System;
namespace MetaTechture.Workflow
{
/// <summary>
/// Represents the fundamental building block of the entire workflow model.
/// The IProcessStep interface is the basic component of a workflow process, as
/// it represents a single step in any process. This interface depicts
/// the basic structure of any step in a larger process that an
/// application must execute.
/// </summary>
public interface IProcessStep
{
/// <summary>
/// Any process step must contain independent functionality
/// that it shall execute. Within this method, the functionality
/// should reside.
/// </summary>
/// <remarks>
/// The point of this class is simple. As an object moves through
/// any process, the object's state may alter or be altered as
/// a result of the actions within the step. Any class which
/// executes multiple steps should not only account for the step
/// itself, but the change to the object's state as the
/// step occurs. To persist state, it is good practice for the
/// object to return itself from the execution. In essence,
/// the next step can adapt to the new state, and the process
/// can continue.
/// </remarks>
/// <returns></returns>
void Execute(ProcessEngine engineContext);
/// <summary>
/// Within any process, a step can have adverse affects on the overall
/// execution of the process. As each step occurs, there is the
/// potential for error, potential for the process to complete,
/// and so on. As each step occurs, this property should be
/// set so that the engine knows whether to continue in the
/// process or to terminate it's execution after this step.
/// </summary>
bool HaltsProcess { get; }
}
}
This interface's sole purpose is to mark a class as a “step” in a longer “process.” I'll make use of this idea later in a relatively simplistic process to exemplify the idea. For now, think of it from two points of view.
Processes are Recipes with Little Ingredients (but room is left for variance)
Everything has to be done with steps. You get from your house to the car, you take steps, then open the car door, then shut it, and on and on. You make a PB&J, you have to do things in your own certain order (some of us like the PB before the J, others vice versa, for instance). With this notion in mind, I'll move forward and point out the next part of the workflow idea - the process engine. To begin with, the process engine contains an internal Hashtable that I'll use in much the same way that the HttpCache is used. As a process executes other objects within a subsystem will be affected in appropriate measures. For this, we'll use the Hashtable collection:
using System;
using System.Collections;
namespace MetaTechture.Workflow
{
/// <summary>
/// Runs the collection of steps for a given process against a particular
/// object instance.
/// </summary>
public class ProcessEngine
{
Hashtable ctx;
When the ProcessEngine class is constructed I'll get the Hashtable ready for usage by declaring it as a new object. To boot, I'll add a Facade around the internal collection by adding an AddToApplicationContext method and an indexer.
/// we won't allow construction, as the static Run method will be enough.
public ProcessEngine()
{
ctx = new Hashtable();
}
public int AddToApplicationContext(object key, object value)
{
ctx.Add(key,value);
return ctx.Count;
}
public object this[object key]
{
get { return ctx[key]; }
set { ctx[key] = value; }
}
This way, Process Step classes can re-use objects that have been stashed in the Process Engine's execution layer (as you'll see later on, this allows the steps interact with one another in much the same way we work with other caching methodologies).
Processes Should be Able to Stop Themselves
In the final stages of the ProcessEngine class, I'll add logic that will once again use the configuration capabilities in .Net to load in the steps for a process. This stage of the code creates instances of classes that inherit the IProcessStep interface, and will add these items to the overall process in the order they appear in the configuration file....
Which raises an interesting point! Suppose, from our earlier example of making a PB&J, we have a requirement stating the order things should happen -
- Step 1: Get the Bread
- Step 2: Apply Peanut Butter
- Step 3: Apply Jelly
And you write code implementing this functionality - or rather, you hard-code a method executing this functionality. Then, one day, the world turns upside-down and you learn that in these new days of jellyloving sandwich-eaters, people like their jelly applied before the peanut butter. Guess what? Your method(s) now need to be rewritten to accomodate the new functionality. Sure, each method (GetBread(), ApplyPb(), and ApplyJelly(), if you've been a good refactorer up to this point) may be wonderfully mutually exclusive, but since you're executing these according to the original specification, you now need to edit, recompile, and re-deploy your existing code. With the workflow resident in MT (via .Net's configuration wonders), this problem will vanish!
So back to the process itself! At this point, we've covered everything except the Run() method, which basically uses the configuration classes shown below to load and Execute() each step. Take notice esepcially here of how the method performs a check to see if each step of the process is supposed to Halt the process. In this implementation, you can see the beginnings of a control - should one step cause problems in the process - or better yet, complete it earlier than expected - cancel everything from there on out. This leaves the decision-making logic up to each step in the process. Here's the code:
/// <summary>
/// Executes the collection of providers' functions.
/// </summary>
public void Run(string process)
{
// get the name of the type of provider we're supposed to use
string typeName = String.Empty;
ProcessEngineManagerSettings settings =
ProcessEngineManagerSettings.GetSettings(process);
for(int i=0; i<settings.TypesInProcess.Length; i++)
{
typeName = settings.TypesInProcess[i];
// create an instance of that type
Type t = Type.GetType(typeName);
// now create an instance of that type
object provider = null;
provider = Activator.CreateInstance(t);
// make sure it's an implementor of the proivider interface
IProcessStep step = provider as IProcessStep;
if(step != null)
{
// perform it's functionality
step.Execute(this);
if(step.HaltsProcess)
break;
}
}
}
}
}
Not too bad - just create an instance of each class listed in the custom configuration section, in the order we find them all. To finish up this discussion, take a look at the configuration classes, which are listed below:
using System;
using cs = System.Configuration.ConfigurationSettings;
using System.Xml;
namespace MetaTechture.Workflow
{
/// <summary>
/// The configuration reader
/// </summary>
public class ProcessEngineManagerConfigurationReader : System.Configuration.IConfigurationSectionHandler
{
public object Create(object parent,
object context,
XmlNode section)
{
XmlNodeList lstActions = section.SelectNodes("//ExecutionProvider");
return new ProcessEngineManagerSettings(lstActions);
}
}
/// <summary>
/// The settings class, which contains the type name to be loaded.
/// </summary>
public class ProcessEngineManagerSettings
{
public readonly string[] TypesInProcess;
static string activeProcess = String.Empty;
internal ProcessEngineManagerSettings(XmlNodeList lst)
{
string[] types = new string[0];
for(int i=0; i<lst.Count; i++)
{
if(lst[i].Attributes["process"].Value.ToLower() == activeProcess.ToLower())
{
string[] tTmp = new string[types.Length+1];
Array.Copy(types,0,tTmp,0,types.Length);
tTmp[types.Length] = lst[i].Attributes["type"].Value;
types = tTmp;
}
}
TypesInProcess = types;
}
/// <summary>
/// Build the name of the path to the nodes we'll run.
/// </summary>
/// <returns></returns>
public static ProcessEngineManagerSettings GetSettings(string process)
{
string section = "ProcessProviders";
activeProcess = process;
ProcessEngineManagerSettings b =
(ProcessEngineManagerSettings)cs.GetConfig(section);
return b;
}
}
}
Once again making use of the Provider concept, we've assumed that, within the configuration file, a series of steps have been listed. These steps, when executed in appropriate order, complete a total process' execution. The XML below is a good example of how a process could be listed and executed within an application making use of the MT workflow concept.
<?
xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="ProcessProviders" type="MetaTechture.Workflow.ProcessEngineManagerConfigurationReader, MetaTechture.Workflow" />
</configSections>
<!-- process-specific application settings -->
<ProcessProviders>
<!-- steps in various processes -->
<ExecutionProvider type="MetaTechture.Workflow.BreadStep, MetaTechture.ConsoleClients" process="MakeSandwich" />
<ExecutionProvider type="MetaTechture.Workflow.JellyStep, MetaTechture.ConsoleClients" process="MakeSandwich" />
<ExecutionProvider type="MetaTechture.Workflow.PeanutButterStep, MetaTechture.ConsoleClients" process="MakeSandwich" />
</ProcessProviders>
</configuration>
At this point, we've listed a few steps in the process section for a small application. This small little app will make a nice demonstration of using the following three classes - each of which representing or objectifying a small part of a larger process.
using System;
namespace MetaTechture.Workflow
{
/// <summary>
/// The first step in the process.
/// </summary>
public class BreadStep : IProcessStep
{
public void Execute(ProcessEngine engine)
{
Console.WriteLine("Get the Bread");
}
public bool HaltsProcess
{
get
{
return false;
}
}
}
/// <summary>
/// The second step in the process.
/// </summary>
public class PeanutButterStep : IProcessStep
{
public void Execute(ProcessEngine engine)
{
Console.WriteLine("Put PB on the bread");
}
public bool HaltsProcess
{
get
{
return false;
}
}
}
/// <summary>
/// The second step in the process.
/// </summary>
public class JellyStep : IProcessStep
{
public void Execute(ProcessEngine engine)
{
Console.WriteLine("Put jelly on the bread");
}
public bool HaltsProcess
{
get
{
return false;
}
}
}
}
For now, I'll save the more complex idea of working with the context for a later, more comprehensive example. The thing here is to notice - that each step of the process completes one small aspect of the larger, wholer process. In a sense, we've refactored the entire process of making a sandwich into seperate classes, thereby giving structure and shape to activity. In this way, even the process itself has been objectified and therefore, is extensible in and of itself. Should someone decide that we need to change the PB&J order, the re-coding is minimal - just swap two lines in the config file, and the whole application is altered on the fly. A process could be added to or removed in the same method. We'll make use of this heavily in the study at the end of the post series. For now, let's wrap it all up by taking a look at the console application which kicks off the whole MakeSandwich process.
using System;
using MetaTechture.Workflow;
namespace MetaTechture.ConsoleClients
{
/// <summary>
/// Summary description for Class1.
/// </summary>
class Class1
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main(string[] args)
{
RunSandwichStepDemo();
Console.ReadLine();
}
static void RunSandwichStepDemo()
{
ProcessEngine engine = new ProcessEngine();
engine.Run("MakeSandwich");
}
}
}
Happy coding! Next up - a plugin model!
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.
No, Robert, that's not what I said. Let me rephrase what I did say during our chat this afternoon (or at least what you should have known I meant). What Robert should have quote me as saying was “There are so many screwed up parts of the Framework, but it's SO money that you just have to deal with it.”
Isn't this true for any framework? We've all got our gripes and wants, but dude, the Framework is incredible. I'd have to say that I'd almost go as far as to quote Don Box when he said “COM is Love.” DB, if COM is Love, then .NET must be a night of unbridled passion with <Person>.
I found
Code Highlighter at CSharpFriends.com this evening. I can't recomend it highly enough!
To continue with the architectural examination of MT and get into the code, I'll examine the least theoretical - the Data Access Layer. Not going to spend a whole lot of time with a long-winded OO discussion or a review of the GOF patterns here. Instead, I'll get started by first showing the code for the basis of the entire DAL - the IDbLayer interface. Some of these methods might look relatively simple (and similar if you've used the SqlHelper class from the Data Access Building Block). The concept - known by GOF nuts as the Facade Pattern - is something used quite heavily within .NET. Namely, all of the DataAdapters do a pretty good job at the Facade pattern by hiding all of the connection, command-building, other random database processes. Just by calling the Fill() method, for instance, a connection is opened and managed, a command built, data retrieved, connection cut, and dataset populated. We'll use this pattern a bit to obstruct the internal workings of some pretty universal units of functionality, as exemplified in the interface code below.
using System;
using System.Data;
namespace GenericDal
{
public interface IDbLayer
{
/// <summary>
/// The connection string used to connect to a database.
/// </summary>
string ConnectionString { get; set; }
/// <summary>
/// Executes the provided commandText SQL query, builds a DataSet, and returns
/// the DataSet to the calling code. The array of IDbDataParameter objects are added
/// to an internal IDbCommand object's Parameters collection prior to the execution.
/// </summary>
/// <param name="commandText"></param>
/// <param name="commandType"></param>
/// <param name="parameters"></param>
/// <returns></returns>
DataSet ExecuteDataSet(string commandText,
CommandType commandType,
params IDbDataParameter[] parameters);
/// <summary>
/// Executes the provided commandText SQL query
/// and returns a DataReader to the calling code. The array of IDbDataParameter objects are added
/// to an internal IDbCommand object's Parameters collection prior to the execution.
/// </summary>
/// <param name="commandText"></param>
/// <param name="commandType"></param>
/// <param name="parameters"></param>
/// <returns></returns>
IDataReader ExecuteReader(string commandText,
CommandType commandType,
params IDbDataParameter[] parameters);
/// <summary>
/// Executes the provided commandText SQL query
/// and returns a DataReader to the calling code. The array of IDbDataParameter objects are added
/// to an internal IDbCommand object's Parameters collection prior to the execution.
/// </summary>
/// <param name="commandText"></param>
/// <param name="commandType"></param>
/// <param name="parameters"></param>
/// <returns></returns>
IDataReader ExecuteReader(IDbTransaction transaction,
string commandText,
CommandType commandType,
params IDbDataParameter[] parameters);
/// <summary>
/// Executes a SQL procedure and returns a single value.
/// </summary>
/// <param name="commandText"></param>
/// <param name="commandType"></param>
/// <param name="parameters"></param>
/// <returns></returns>
object ExecuteScalar(string commandText,
CommandType commandType,
params IDbDataParameter[] parameters);
/// <summary>
/// Executes a SQL procedure and returns nothing.
/// </summary>
/// <param name="commandText"></param>
/// <param name="commandType"></param>
/// <param name="parameters"></param>
void ExecuteNonQuery(string commandText,
CommandType commandType,
params IDbDataParameter[] parameters);
/// <summary>
/// Executes a SQL procedure and returns nothing.
/// </summary>
/// <param name="commandText"></param>
/// <param name="commandType"></param>
/// <param name="parameters"></param>
void ExecuteNonQuery(IDbTransaction transaction,
string commandText,
CommandType commandType,
params IDbDataParameter[] parameters);
/// <summary>
/// Executes a SQL procedure and returns a single value.
/// </summary>
/// <param name="commandText"></param>
/// <param name="commandType"></param>
/// <param name="parameters"></param>
/// <returns></returns>
object ExecuteScalar(IDbTransaction transaction,
string commandText,
CommandType commandType,
params IDbDataParameter[] parameters);
/// <summary>
/// Returns a list of parameters for a given stored procedure.
/// </summary>
/// <param name="commandText"></param>
/// <returns></returns>
IDataParameterCollection GetCommandParameters(string commandText);
}
}
Not too difficult to surmise what we're doing in this section of the code. Nothing too complex to grasp, really, from a conceptual frame of reference. We've accomodated most of the widely-used access methods of getting to databases - disconnected scenarios (via the DataSet-related methods) and connected (via the DataReader-specific methods). In addition, there's a few overloads to accomodate the existence of IDbTransaction objects to make sure implementors provide some sort of support for database transactions that span multiple method calls.
Now, we'll examine an implementor of this class. To make the discussion a little simple, let's take a look at the SqlServerDal class, which obviously deals with the SqlClient-specific implementations of these methods.
using System;
using System.Data;
using System.Data.SqlClient;
namespace GenericDal
{
public class SqlServerDal : IDbLayer
{
private string cnStr = String.Empty;
public SqlServerDal()
{
}
public string ConnectionString
{
get { return cnStr; }
set { cnStr = value; }
}
/// <summary>
/// Adds parameters to a command object.
/// </summary>
/// <param name="cmd"></param>
/// <param name="parameters"></param>
void PrepareParameters(IDbCommand cmd, params IDbDataParameter[] parameters)
{
if(parameters != null)
{
// add the parameters to the SelectCommand.
foreach(IDbDataParameter p in parameters)
{
cmd.Parameters.Add((SqlParameter)p);
}
}
}
/// <summary>
/// Executes the provided commandText SQL query, builds a DataSet, and returns
/// the DataSet to the calling code. The array of SqlParameter objects are added
/// to an internal SqlCommand object's Parameters collection prior to the execution.
/// </summary>
/// <param name="command"></param>
/// <param name="commandText"></param>
/// <param name="commandType"></param>
/// <param name="parameters"></param>
/// <returns></returns>
public DataSet ExecuteDataSet(string commandText,
System.Data.CommandType commandType,
params IDbDataParameter[] parameters)
{
SqlConnection cn = new SqlConnection(this.ConnectionString);
// instantiate a new dataadapter to fill up the dataset
SqlDataAdapter daTmp = new SqlDataAdapter(commandText,cn);
daTmp.SelectCommand.CommandType = commandType;
PrepareParameters(daTmp.SelectCommand, parameters);
// return the dataset
DataSet dsTmp = new DataSet();
daTmp.Fill(dsTmp);
return dsTmp;
}
/// <summary>
/// Executes the provided commandText SQL query
/// and returns a DataReader to the calling code. The array of IDbDataParameter objects are added
/// to an internal IDbCommand object's Parameters collection prior to the execution.
/// </summary>
/// <param name="command"></param>
/// <param name="commandText"></param>
/// <param name="commandType"></param>
/// <param name="parameters"></param>
/// <returns></returns>
public IDataReader ExecuteReader(string commandText,
System.Data.CommandType commandType,
params IDbDataParameter[] parameters)
{
SqlConnection cn = new SqlConnection(this.ConnectionString);
SqlCommand cmd = new SqlCommand(commandText,cn);
cmd.CommandType = commandType;
cmd.Connection = cn;
PrepareParameters(cmd, parameters);
cn.Open();
SqlDataReader rdr = cmd.ExecuteReader(CommandBehavior.CloseConnection);
return rdr;
}
/// <summary>
/// Executes the provided commandText SQL query
/// and returns a DataReader to the calling code. The array of IDbDataParameter objects are added
/// to an internal IDbCommand object's Parameters collection prior to the execution.
/// </summary>
/// <param name="commandText"></param>
/// <param name="commandType"></param>
/// <param name="parameters"></param>
/// <returns></returns>
public IDataReader ExecuteReader(IDbTransaction transaction, string commandText,
CommandType commandType,
params IDbDataParameter[] parameters)
{
SqlConnection cn = new SqlConnection(this.ConnectionString);
SqlCommand cmd = new SqlCommand(commandText,cn,(SqlTransaction)transaction);
cmd.CommandType = commandType;
cmd.Connection = cn;
cmd.Transaction = (SqlTransaction)transaction;
PrepareParameters(cmd, parameters);
cn.Open();
SqlDataReader rdr = cmd.ExecuteReader(CommandBehavior.CloseConnection);
return rdr;
}
/// <summary>
/// Executes a SQL procedure and returns a single value.
/// </summary>
/// <param name="commandText"></param>
/// <param name="commandType"></param>
/// <param name="parameters"></param>
/// <returns></returns>
public object ExecuteScalar(string commandText,
CommandType commandType,
params IDbDataParameter[] parameters)
{
SqlConnection cn = new SqlConnection(this.ConnectionString);
SqlCommand cmd = new SqlCommand(commandText,cn);
cmd.CommandType = commandType;
cmd.Connection = cn;
PrepareParameters(cmd, parameters);
cn.Open();
object o = cmd.ExecuteScalar();
cn.Close();
return o;
}
/// <summary>
/// Executes a SQL procedure and returns a single value.
/// </summary>
/// <param name="commandText"></param>
/// <param name="commandType"></param>
/// <param name="parameters"></param>
/// <returns></returns>
public object ExecuteScalar(IDbTransaction transaction,
string commandText,
System.Data.CommandType commandType,
params IDbDataParameter[] parameters)
{
SqlConnection cn = new SqlConnection(this.ConnectionString);
SqlCommand cmd = new SqlCommand(commandText,cn,(SqlTransaction)transaction);
cmd.CommandType = commandType;
cmd.Connection = cn;
cmd.Transaction = (SqlTransaction)transaction;
PrepareParameters(cmd, parameters);
cn.Open();
object o = cmd.ExecuteScalar();
cn.Close();
return o;
}
public void ExecuteNonQuery(string commandText,
CommandType commandType,
params IDbDataParameter[] parameters)
{
SqlConnection cn = new SqlConnection(this.ConnectionString);
SqlCommand cmd = new SqlCommand(commandText,cn);
cmd.CommandType = commandType;
cmd.Connection = cn;
PrepareParameters(cmd, parameters);
cn.Open();
cmd.ExecuteNonQuery();
cn.Close();
}
public void ExecuteNonQuery(IDbTransaction transaction,
string commandText,
System.Data.CommandType commandType,
params IDbDataParameter[] parameters)
{
SqlConnection cn = new SqlConnection(this.ConnectionString);
SqlCommand cmd = new SqlCommand(commandText,cn,(SqlTransaction)transaction);
cmd.CommandType = commandType;
cmd.Connection = cn;
cmd.Transaction = (SqlTransaction)transaction;
PrepareParameters(cmd, parameters);
cn.Open();
cmd.ExecuteNonQuery();
cn.Close();
}
public IDataParameterCollection GetCommandParameters(string commandText)
{
SqlConnection cn = new SqlConnection(this.ConnectionString);
cn.Open();
SqlCommand cmd = new SqlCommand(commandText,cn);
cmd.CommandType = CommandType.StoredProcedure;
SqlCommandBuilder.DeriveParameters(cmd);
cn.Close();
return cmd.Parameters;
}
}
}
You may opt to perform some extra logic in some of these methods. Myself, I like to keep the base implementations as simple as possible and account for things later on in more specific code. For example, if any of these methods throw an instance of a SqlException caused via a SQL Server RAISERROR() command, I'd most likely want to account for that in my client or middleware code.
At this point, you could basically cut and paste this minimal functionality into a new class named OleDbDal and change all of the appropriate places. It may not be optimized to the hills, but the point is simple - create a base structure and defer the internal workings of the methods to higher-level classes. That way, if need be later on, the same class' methods could be overridden and extended even further. The code sample below is just that - a copy-and-paste-and-find-and-replace, resulting in the most basic second implementation.
using System;
using System.Data;
using System.Data.OleDb;
namespace GenericDal
{
public class OleDbDal : IDbLayer
{
private string cnStr = String.Empty;
public OleDbDal()
{
}
public string ConnectionString
{
get { return cnStr; }
set { cnStr = value; }
}
/// <summary>
/// Adds parameters to a command object.
/// </summary>
/// <param name="cmd"></param>
/// <param name="parameters"></param>
void PrepareParameters(IDbCommand cmd, params IDbDataParameter[] parameters)
{
if(parameters != null)
{
// add the parameters to the SelectCommand.
foreach(IDbDataParameter p in parameters)
{
cmd.Parameters.Add((OleDbParameter)p);
}
}
}
/// <summary>
/// Executes the provided commandText OleDb query, builds a DataSet, and returns
/// the DataSet to the calling code. The array of OleDbParameter objects are added
/// to an internal OleDbCommand object's Parameters collection prior to the execution.
/// </summary>
/// <param name="command"></param>
/// <param name="commandText"></param>
/// <param name="commandType"></param>
/// <param name="parameters"></param>
/// <returns></returns>
public DataSet ExecuteDataSet(string commandText,
System.Data.CommandType commandType,
params IDbDataParameter[] parameters)
{
OleDbConnection cn = new OleDbConnection(this.ConnectionString);
// instantiate a new dataadapter to fill up the dataset
OleDbDataAdapter daTmp = new OleDbDataAdapter(commandText,cn);
daTmp.SelectCommand.CommandType = commandType;
PrepareParameters(daTmp.SelectCommand, parameters);
// return the dataset
DataSet dsTmp = new DataSet();
daTmp.Fill(dsTmp);
return dsTmp;
}
/// <summary>
/// Executes the provided commandText OleDb query
/// and returns a DataReader to the calling code. The array of IDbDataParameter objects are added
/// to an internal IDbCommand object's Parameters collection prior to the execution.
/// </summary>
/// <param name="command"></param>
/// <param name="commandText"></param>
/// <param name="commandType"></param>
/// <param name="parameters"></param>
/// <returns></returns>
public IDataReader ExecuteReader(string commandText,
System.Data.CommandType commandType,
params IDbDataParameter[] parameters)
{
OleDbConnection cn = new OleDbConnection(this.ConnectionString);
OleDbCommand cmd = new OleDbCommand(commandText,cn);
cmd.CommandType = commandType;
cmd.Connection = cn;
PrepareParameters(cmd, parameters);
cn.Open();
OleDbDataReader rdr = cmd.ExecuteReader(CommandBehavior.CloseConnection);
return rdr;
}
/// <summary>
/// Executes the provided commandText OleDb query
/// and returns a DataReader to the calling code. The array of IDbDataParameter objects are added
/// to an internal IDbCommand object's Parameters collection prior to the execution.
/// </summary>
/// <param name="commandText"></param>
/// <param name="commandType"></param>
/// <param name="parameters"></param>
/// <returns></returns>
public IDataReader ExecuteReader(IDbTransaction transaction, string commandText,
CommandType commandType,
params IDbDataParameter[] parameters)
{
OleDbConnection cn = new OleDbConnection(this.ConnectionString);
OleDbCommand cmd = new OleDbCommand(commandText,cn,(OleDbTransaction)transaction);
cmd.CommandType = commandType;
cmd.Connection = cn;
cmd.Transaction = (OleDbTransaction)transaction;
PrepareParameters(cmd, parameters);
cn.Open();
OleDbDataReader rdr = cmd.ExecuteReader(CommandBehavior.CloseConnection);
return rdr;
}
/// <summary>
/// Executes a OleDb procedure and returns a single value.
/// </summary>
/// <param name="commandText"></param>
/// <param name="commandType"></param>
/// <param name="parameters"></param>
/// <returns></returns>
public object ExecuteScalar(string commandText,
CommandType commandType,
params IDbDataParameter[] parameters)
{
OleDbConnection cn = new OleDbConnection(this.ConnectionString);
OleDbCommand cmd = new OleDbCommand(commandText,cn);
cmd.CommandType = commandType;
cmd.Connection = cn;
PrepareParameters(cmd, parameters);
cn.Open();
object o = cmd.ExecuteScalar();
cn.Close();
return o;
}
/// <summary>
/// Executes a OleDb procedure and returns a single value.
/// </summary>
/// <param name="commandText"></param>
/// <param name="commandType"></param>
/// <param name="parameters"></param>
/// <returns></returns>
public object ExecuteScalar(IDbTransaction transaction,
string commandText,
System.Data.CommandType commandType,
params IDbDataParameter[] parameters)
{
OleDbConnection cn = new OleDbConnection(this.ConnectionString);
OleDbCommand cmd = new OleDbCommand(commandText,cn,(OleDbTransaction)transaction);
cmd.CommandType = commandType;
cmd.Connection = cn;
cmd.Transaction = (OleDbTransaction)transaction;
PrepareParameters(cmd, parameters);
cn.Open();
object o = cmd.ExecuteScalar();
cn.Close();
return o;
}
public void ExecuteNonQuery(string commandText,
CommandType commandType,
params IDbDataParameter[] parameters)
{
OleDbConnection cn = new OleDbConnection(this.ConnectionString);
OleDbCommand cmd = new OleDbCommand(commandText,cn);
cmd.CommandType = commandType;
cmd.Connection = cn;
PrepareParameters(cmd, parameters);
cn.Open();
cmd.ExecuteNonQuery();
cn.Close();
}
public void ExecuteNonQuery(IDbTransaction transaction,
string commandText,
System.Data.CommandType commandType,
params IDbDataParameter[] parameters)
{
OleDbConnection cn = new OleDbConnection(this.ConnectionString);
OleDbCommand cmd = new OleDbCommand(commandText,cn,(OleDbTransaction)transaction);
cmd.CommandType = commandType;
cmd.Connection = cn;
cmd.Transaction = (OleDbTransaction)transaction;
PrepareParameters(cmd, parameters);
cn.Open();
cmd.ExecuteNonQuery();
cn.Close();
}
public IDataParameterCollection GetCommandParameters(string commandText)
{
OleDbConnection cn = new OleDbConnection(this.ConnectionString);
cn.Open();
OleDbCommand cmd = new OleDbCommand(commandText,cn);
cmd.CommandType = CommandType.StoredProcedure;
OleDbCommandBuilder.DeriveParameters(cmd);
cn.Close();
return cmd.Parameters;
}
}
}
At this point, we've accounted for the databases support by the SqlClient namespace and the OleDb namespace. Not bad for a few minutes' work, eh? The next post will focus on the next step in the DAL - centralizing the logic in such a way that a config file can be used to name a series or collection of IDbLayer implementors in much the same way that the appSetting section works. Think of it this way - Microsoft Windows has a nice, high-level data layer built into it: ODBC - a method of using named “pointers” to connections lower in the OS so that your applications' data access can be abstracted somewhat. This DAL will take that idea and use it on a per-application basis.
I've been thinking for some time about architectural ideas. I was all about Session's concept of the SOA, the Fiefdom/Emissary model put forth by one of the fellow webloggers, and all of the MS Patterns and Practices documents that've been put forth.
Some time ago I wrote a post about using the Provider model for workflow, and my idea-basket began to fill up relatively rapidly. Since that posting, I've been working a lot, drawing pictures, and conceptualizing an architecture that, if implemented properly, would accomodate a few goals I've had in my brain for the past few years.
The Origin:
It all began with a particular client who told me that their application had one single business rule:
There are no business rules. Our users will be making them up as they go along.
You can imagine the blank stares that soon stretched across my face and the face of my colleague, who were the sole proprieters of this doomed idea. From the very outset of the project, I knew I'd have to come up with something just shy of impossible - a system that would allow for change at any point in its lifecycle, according to the needs of the users, on a moment's notice.
I still hold true that such an architecture is next to impossible, but with a little thought, a lot of creativity, and a whole bunch of pictures that form the mess strewn about my apartment, I think may have come somewhat close to the beginnings of such an architecture, and I've chosen this forum as a place to unleash it onto the world, request feedback, evoke some heated debate, and hopefully, make it pretty awesome.
What do I Intend to Solve?
I think that every good idea begins as frustration. Meaning, you find yourself frustrated by something over and over again. You think of ideas to solve the problem, and run aground time and time again as you sail around looking for answers. It is in this frustrated blur that the answer escapes you, and only later on, when you're clear-minded, can you look back on the point of frustration and find a solution that makes sense. This architectural idea - what I'll refer to from here on out as the MetaTechture (an architecture “about” or “for” other architectures, plus it seemed pretty catchy when I first said it) - began as a by-product of these frustrations. In fact, the core components of MT arose one by one as a result of my repeated frustrations with certain aspects of enterprise development. Given the fact that these frustrations were the major contributing factor in my decision to spend facets of my life for about 3 years drawing, reading, researching, and thinking about how I could accomodate these prolific problems, I'll begin by presenting these major complaints for you, and address each independently in terms of how “it would be nice” to be able to forever solve the problem in and of itself.
Problem #1: Requirements Change (or just plan suck right off the bat)
We all know the pain of changing requirements. Alan Cooper claims it to be the major hurdle in any and all application development paradigms. Each and every colleague with whom I've had a beer begins to tell their own version of the same horror story at some point during the conversation. It is, sadly, a fact of life for those of us who make this industry our home. Sometimes you get them up front and they make sense. Whether a result of process alteration (during your development time on any given project), poor communication (even if you do use XP properly), or the old favorite “now that we see it, we have additional ideas we'd like to see implemented” (wink wink at the client), this factor is the major contributor to MT. To accomodate this problem into MT, I'll devise a way of controlling flow - workflow, if you will - throughout a system; in fact, I'll objectify the very concept of a process step within the MT framework and introduce the notion of changing processes on the fly.
Problem #2: Code has to be Extended Later
Most clients realize this potential problem. They hire us, we write the application, it exceeds their expectations (and requirements, since they probably fell prey to Problem #1), and we move on. Then, the client realizes that they have virtually no idea, despite how awesome our documentation may be, on how to extend or maintain the application. You've probably been on at least one end of this conversation:
Customer: “What happens when our business processes change? Can the application deal with that?“
Developer: [Stammers] “Well, that depends...“
Customer: “On?“
Developer: [More Stammer, then provide the formula response] “On how complex the process change is. If we've hardcoded logic in the places where those changes are emulated within the system, the code will have to be altered.“
Customer: “Is that included in your maintenance agreement?“
And so forth and so on, you know the drill. MT will attempt to deliver us all from this evil by way (you should have see this coming) of the Plugin concept, delivered by the Provider model. I'll take a look at the notion of a plugin, when it should be used, and outline their use under the MT model. Then, I'll point out how MT allows flexibility in terms of how plugins are located and loaded into the application's execution model.
Problem #3: Too Many Database Platforms, Too Little Time
Sure, this one isn't so big as a result of .NET's limitless support for virtually any database driver/structure/idea. With support for everything all the way back to the ODBC world, we've got a nice little series of packages we can use to access databases. This problem, of course, gives rise to the concept of a Generic Data Access Layer. MT has a component which allows for this paradigm, and as a result of being the simplest-to-understand, will be the first component covered in this series. I'll level .NET's use of OO, the old concepts of Interface Implementation, and tie it all together with a nice dosage of the Provider model to make it easy for your applications to talk to any database - or a farm of them if needed.
With these problems out of the way, I'll posit perhaps the most important aspect of MT:
When you think like a computer, you fail to address the problem resident in most business logic: persistent alteration and change. You can't set a program into motion with a few-hundred-or-so rules and expect that, when those rules fail to validate as you expected, the program will continue to execute. It isn't very real-world of you, now is it? The world isn't written in ones and zeroes, and your application logic should expect and deal with flies in the ointment.
MT will attempt to solve these stated problems. Hopefully the readers of this series will put forth some experiences of their own to add to the problem list, thereby only making MT a more founded, solid architectural idea. From here, the posts in this series will traverse down the ladder of architectural complexity.
First off, I'll try to appease Problem #3. In the days to come, the second article in the MetaTechture series will put forth a relatively simplistic data access architecture, somewhat based on the SqlHelper class presented by the Data Access Building Block. From that point, I'll discuss the notion presented originally in this post, which focuses on dynamic workflow control via the Provider model. I've expanded this idea almost to the point of being application-server-ish, and will attempt to explain the idea pretty clearly through a few individual posts, which explain each aspect of the workflow model in MT. Finally, I'll go into some length explaining the Plugin framework within MT, and discuss the aspects of flexible plugin loading, assembly inspection, and so forth. Finally, I'll exemplify all of these aspects in a single, simple case study which will facilitate each aspect of MT to exemplify its usefullness.
And yes, I invite any flames, suggestions, bashing, or “so-and-so product already does that.” Yet, my hope is that I can inspire you through creative use of OO concepts, a little elbow grease, and some old-fashioned head-down coding.
More Posts
Next page »