Click here to Skip to main content
Email Password   helpLost your password?

assembly_graph.jpg

Introduction

I have implemented this class library during the development of several database-driven projects. Some of these projects utilized large amounts of data, and it was needed to implement a fast working and easy to use data access layer. I will not describe my reasons why to not use typed DataSets, NHibernate, DLINQ, or something similar (but if you are interested, here I've described some of them). I think that if you are reading this article you have your own ones.

Instead, I'll describe what this class library provides:

Strongly typed SQL commands

I've implemented support for two SQL command types: select queries (EntityReaderCommand<TEntity>) and scripts (EntityScriptCommand<TEntity>).

With EntityReaderCommand<TEntity>, you can read entities from the database by executing a specified SQL query. It works by analogy with IDataReader implementations, but when the IDataReader with each fetch returns an IDataRecord, the EntityReaderCommand<TEntity> returns an instance of the TEntity class.

This code sample will fill List<Person> from the database table "Person":

var persons = new List<Person>();
var reader = new EntityReaderCommand<Person>(
   args => persons.Add(args.Entity),
   @"
SELECT
      Id <Id>,
      FirstName <FirstName>,
      LastName <LastName>
   FROM
      Person");
reader.Execute();

This code sample will read a Rectangle instance from the database:

var rectangle = new Rectangle();
new EntityReaderCommand<Rectangle>(
   delegate(EntityReaderArguments<Rectangle> args)
   {
      rectangle = args.Entity;
      args.Terminate = true;
   },
   @"
SELECT
      1 <Location.X>,
      2 <Location.Y>,
      3 <Width>,
      4 <Height>").Execute();

This code sample does the same, but using EntityScriptCommand<TEntity>:

var command = new EntityScriptCommand<Rectangle>(
   new Rectangle(),
   @"
SET <Location.X> = 1
SET <Location.Y> = 2
SET <Width> = 3
SET <Height> = 4");
command.Execute();
var rectangle = command.Entity;

The following code sample inserts a new Person instance in the database table "Person". Note, the Person object will have a new Id after insert.Execute().

var person = new Person{
   FirstName = "Boris",
   LastName = "Nadezhdin"
};
var insert = new EntityScriptCommand<Person>(
   person,
   @"
SET <Id> = newid()
INSERT INTO
   Person
   (
      Id,
      FirstName,
      LastName
   )
   VALUES
   (
      <Id>,
      <FirstName>,
      <LastName>
   )");
insert.Execute();

So, as you can see from the samples above, the query syntax is native to the SQL client, the only difference is mapping between the entity properties and the column aliases. Commands support composite properties (like Rectangle.Location). Entities and their properties can be reference or value types. There is no need for special XML or attribute based O/R mappings, all mappings are specified directly in the queries.

There are only two restrictions on the types of entities and their properties:

Disconnected entities

There is no proxy generation for entities in the command execution logic, so if you are using an EntityReaderCommand<TEntity>, you will read the TEntity, not a generic ancestor.

There is no constraints on entities and session scopes, you can read an entity from the database in one session and update it back in another one.

There is no entity cache, so you can have several instances of the same database entity.

Simple syntax for command parameters

Sometimes it needs to pass some additional arguments into the command. For example, to find a Person by Id:

public Person FindOne(Guid id)
{
   Person person = null;
   var reader = new EntityReaderCommand<Person>(
      delegate(EntityReaderArguments<Person> args)
      {
         person = args.Entity;
         args.Terminate = true;
      },
      @"
   SELECT
         Id <Id>,
         FirstName <FirstName>,
         LastName <LastName>
      FROM
         Person
      WHERE
         Id = {0}", id);
   reader.Execute();
   return person;
}

As you can see from the sample above, the syntax is similar to String.Format(String, params Object[] args), but in that case, id is not substituted by its string representation, it is passed into the command as a parameter.

Here is another sample in which you can see how to pass an argument and get its modified value back after the command execution:

const int one = 1;
var command = new SimpleCommand(
   "SET {0} = {0} + 1", one);
command.Execute();
Assert.AreEqual(one + 1, command.GetArgs()[0]);

Transparent session and transaction management

Each command needs an open database session; if there is no open session in the current thread, the command opens a new session and closes it after execution.

To open new session manually, you should create a new instance of SessionScope; to close it, call Dispose() on the instance of the SessionScope. So, all commands in the scope of the SessionScope object will use one session, created by the SessionScope object.

If you are working on a web application and you want to have a database session per web request, you can create an instance of the SessionScope (and store it in items of HttpContext, for example) in the BeginRequest event handler and dispose it in the EndRequest event handler.

In addition to SessionScope, there is another scope of command execution, TransactionScope. It is used for transaction management.

To start a new transaction, you should create a new instance of TransactionScope. To end it, call Dispose() on the instance of the TransactionScope. So, all command in scope of that TransactionScope object will work in the transaction created by the TransactionScope object. Transactions are marked to rollback by default. So, to commit a transaction, you should call Commit() on the instance of the TransactionScope. Note, that no actual commit is performed before the disposal of TransactionScope.

Another interesting feature of command scopes is that they are local to thread there they were instantiated. So commands in different threads will always work in different database sessions.

Native database client abstraction layer

Objects from this class library are not coupled with any concrete database client. Session and transaction management, command execution, all work with native database clients only through the IDatabaseProvider interface. All methods specific to native database clients are extracted in this interface. Currently, there is only one implementation of IDatabaseProvider, for Microsoft SQL Server, but it is really simple to implement ones for Oracle, PgSQL, or MySQL.

The concrete IDatabaseProvider implementation and database connection string can be defined declaratively in the application configuration file, or programmatically via Scope.Configuration. Application configuration file is used by default, for example:

<?xml version="1.0"?>
<configuration>
   <configSections>
      <section
         name="dataAccess"
         type="Yap.Data.Client.ConfigurationSectionHandler, Yap.Data.Client"/>
   </configSections>

   <dataAccess
      providerType="Yap.Data.Client.Sql.SqlProvider, Yap.Data.Client.Sql"
      providerAssembly="Yap.Data.Client.Sql.dll"
      connectionString="Data Source=.\SQLEXPRESS; Database=YAP;Integrated Security=True;"/>
</configuration>

Sources

The solution was created in Visual Studio 2008, and consists of three projects:

The main object model is defined in Yap.Data.Client. The IDatabaseProvider implementation for Microsoft SQL Server is defined in Yap.Data.Client.Sql. Yap.Data.UnitTest contains test fixtures for the Yap.Data.Client assembly. Yap.Data.UnitTest uses MSTest, but can be easily converted to NUnit.

To run Unit Tests, it needs to create a database, execute create-script.sql from the folder SQL, and modify the connection string in the app.config file.

Binaries

You need only three things to use this class library in your project:

History

You must Sign In to use this message board.
 
 
Per page   
 FirstPrevNext
GeneralMySql
mtnz
17:08 27 Aug '09  
Nice light-weight DAL,

I needed a mySQL version so below is the code

namespace Yap.Data.Client.MySql
{
public class MySqlProvider : IDatabaseProvider
{
#region IDatabaseProvider Members

public virtual IDbConnection GetConnection(String connectionString)
{
var connection = new MySqlConnection(connectionString);
connection.Open();
return connection;
}

public virtual void ReleaseConnection(IDbConnection connection)
{
var sqlConnection = connection as MySqlConnection;
if (sqlConnection != null)
{
sqlConnection.Dispose();
}
}

public virtual IDbCommand CreateCommand()
{
return new MySqlCommand();
}

public virtual void ReleaseCommand(IDbCommand command)
{
var sqlCommand = command as MySqlCommand;
if (sqlCommand != null)
{
sqlCommand.Dispose();
}
}

public virtual void ReleaseReader(IDataReader reader)
{
var sqlDataReader = reader as MySqlDataReader;
if (sqlDataReader != null)
{
sqlDataReader.Dispose();
}
}

public string ParameterPrefix
{
get { return "@"; }
}

public virtual IDataParameter CreateParameter(String name, Object value, Type type, Int32? size, ParameterDirection direction)
{
MySqlDbType dbType = MySqlDbType.VarChar;
Object dbValue = value;

if (type.IsGenericType &&
type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
type = type.GetGenericArguments()[0];
}

if (type.IsEnum)
{
dbType = MySqlDbType.Int32;
dbValue = (Int32)value;
}
else if (type == typeof(Byte))
{
dbType = MySqlDbType.UByte;
}
else if (type == typeof(Byte[]))
{
dbType = MySqlDbType.Binary;
}
else if (
type == typeof(Char) ||
type == typeof(Char[]) ||
type == typeof(String))
{
if (!size.HasValue || size <= 2000)
{
dbType = MySqlDbType.VarChar;
}
else
{
dbType = MySqlDbType.Text;
}
dbValue = value == null ?
(Object) DBNull.Value :
value.ToString();
}
else if (type == typeof(DateTime))
{
dbType = MySqlDbType.DateTime;
}
else if (type == typeof(Decimal))
{
dbType = MySqlDbType.Decimal;
}
else if (type == typeof(Double))
{
dbType = MySqlDbType.Float;
}
else if (type == typeof(Single))
{
dbType = MySqlDbType.Double;
}
else if (
type == typeof(Int16) ||
type == typeof(Boolean))
{
dbType = MySqlDbType.UInt16;
}
else if (type == typeof(Int32))
{
dbType = MySqlDbType.Int32;
}
else if (type == typeof(Int64))
{
dbType = MySqlDbType.Int64;
}
else if (type == typeof(TimeSpan))
{
dbType = MySqlDbType.DateTime;
}
else if (type == typeof(Guid))
{
dbType = MySqlDbType.String;
}

var parameter = new MySqlParameter(name, dbType){
Direction = direction, Value = dbValue
};

if (dbType == MySqlDbType.VarChar)
{
parameter.Size = size.HasValue ? size.Value : 2000;
}

return parameter;
}

public Type ConvertFromDatabase(Type type)
{
return type;
}

public Object ConvertFromDatabase(Object value, Type type)
{
Object convertedValue;
if (value == null || value == DBNull.Value)
{
convertedValue = type.IsValueType ? Activator.CreateInstance(type) : null;
}
else
{
if (type.IsEnum)
{
convertedValue = Enum.ToObject(
type, Convert.ToInt32(value, CultureInfo.CurrentCulture));
}
else if (type.IsGenericType &&
type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
type = type.GetGenericArguments()[0];
convertedValue = Convert.ChangeType(
value, type, CultureInfo.CurrentCulture);
}
else
{
convertedValue = Convert.ChangeType(
value, type, CultureInfo.CurrentCulture);
}
}
return convertedValue;
}

public virtual IDbTransaction BeginTransaction(IDbConnection connection)
{
return connection.BeginTransaction();
}

public virtual void SaveTransaction(IDbTransaction transaction, String savePointName)
{
var tran = transaction as MySqlTransaction;
if (tran == null)
{
throw new InvalidOperationException(
Resources.NoNestedTransactions);
}
tran.Commit();
}

public virtual void RollbackTransaction(IDbTransaction transaction)
{
transaction.Rollback();
}

public virtual void RollbackTransaction(IDbTransaction transaction, String savePointName)
{
var tran = transaction as MySqlTransaction;
if (tran == null)
{
throw new InvalidOperationException(
Resources.NoNestedTransactions);
}
tran.Rollback();
}

public virtual void CommitTransaction(IDbTransaction transaction)
{
transaction.Commit();
}

#endregion
}
}

GeneralRe: MySql
6opuc
2:51 29 Aug '09  
Great!
Thanks for sharing!
GeneralRe: MySql
6opuc
2:59 29 Aug '09  
Sorry for possible misunderstanding, but i think that you should always throw InvalidOperationException in methods RollbackTransaction(IDbTransaction transaction, String savePointName) and SaveTransaction(IDbTransaction transaction, String savePointName) if MySQL does not support nested transactions.

So the code should be as follows:
public virtual IDbTransaction BeginTransaction(IDbConnection connection)
{
return connection.BeginTransaction();
}

public virtual void SaveTransaction(IDbTransaction transaction, String savePointName)
{
throw new InvalidOperationException(
Resources.NoNestedTransactions);
}

public virtual void RollbackTransaction(IDbTransaction transaction)
{
transaction.Rollback();
}

public virtual void RollbackTransaction(IDbTransaction transaction, String savePointName)
{
throw new InvalidOperationException(
Resources.NoNestedTransactions);
}

public virtual void CommitTransaction(IDbTransaction transaction)
{
transaction.Commit();
}
GeneralGood article
Donsw
7:44 16 Jan '09  
Good article, I will downlaod the code and hopefully will get to exxplore more. thanks .
GeneralRe: Good article
6opuc
7:57 16 Jan '09  
Thanks for your interest!
Hope my code will be helpful for YouSmile
Please, let me know if you have any suggestions on improving it!
GeneralMisleading categorization
John Whitmire
8:47 15 Jan '09  
Not much you can do about it now, but the categorizations of this article (in the top banner) include wrong items. Specifically, VS 2005, C# 1.0, and C# 2.0 should not be included. You use language features that aren't available in those products.

Being personally confined for now to VS 2005/C# 2.0, your article makes me hungrier for VS 2008/C# 3.0. How useful it would be as we are about to embark on a DAL development effort. Sigh
GeneralRe: Misleading categorization
6opuc
20:28 15 Jan '09  
Yes, You're right! VS 2005, and C# 1.0 Should not be there!

But You can use my binaries in your C# 2.0 application...
Hope it helpsFrown


Anyway, it is not hard to change C#3.0 -specific code to C#2.0
As I remember, I've used only "var"s from C#3.0 in my code...
Let me know if you'll have problems with this
GeneralMy Dal
rcastrogo
1:18 13 Jan '09  
This is my framework with very much similitudes.

http://www.rcastroblog.blogspot.com/
GeneralRe: My Dal
6opuc
1:40 13 Jan '09  
Thanks for the link!
Very interesting solution, but it much differs from mine... As i understand from examples, client code should define mappings between objects and tables, and framework helper classes generates queries for that classes, right?
I think NHibernate does the same...

But i look on the problem from the different side, i don't want framework to generate SQL, I'll do it myself. All I want is just to simplify work with native SQL.

Sorry for my english Smile
GeneralRe: My Dal
rcastrogo
21:58 13 Jan '09  
The framework don't generate SQL sentences. I do it myself, too.
I use code generation to make CRUD SQL sentences.

I don't like NHibernate. I like do things by myself like you.

My english in not better than yours
Laugh Laugh

greetings from Madrid, Spain
GeneralRe: My Dal
6opuc
0:17 14 Jan '09  
Thanks for clarification!
Great work!
It's always interesting to reinvent the wheel Wink

Greetings to Madrid from Ufa, Russia Smile
GeneralRe: My Dal
oluckyman
0:03 17 Jan '09  
Greetings to Ufa from Moscow %-)
thanx for you work!
GeneralRe: My Dal
6opuc
1:22 17 Jan '09  
Илюха, ну и фотка Smile


Last Updated 11 Jan 2009 | Advertise | Privacy | Terms of Use | Copyright © CodeProject, 1999-2010