The search for a C# DAL code generator that actually works. Part 3: the solution.
September 4, 2008
In part 2 of this series, I outlined my requirements for an ideal DAL generator.
Well, not having found any tool out there that meets my criteria, I ended up writing the tool myself, meeting all but one of my objectives [I dont handle stored procedures yet].
Sql Server 2005 is the target DBMS for now, using VS2005 as the compiler.
Lets walk through how to use the tool.
- From the command line, the tool is invoked like this.
mycodegentool.exe config.xml- The simplest config.xml file is
<CodeGen>
<Directory>D:\development\codegenExample\myDatabase</Directory>
<ConnectionString>Data Source=.;Initial Catalog=myDatabase;
Integrated Security=SSPI;</ConnectionString>
<ConcurrencyColumn>update_date</ConcurrencyColumn>
<CodeGen>- To get a unit test dll as well, add this xml fragment to the config file.
<UnitTest>
<Directory>D:\development\codegenExample\myDatabase\CodeGenUnitTest</Directory>
</UnitTest>This takes care of the plumbing. Now reference the DAL dll from your business layer dll, and happy porcelain coding.
Here are a few use case scenarios in actually using the DAL from your business layer.
- Load up a table.
ProductsFactory factory = new ProductsFactory();
List rows = factory.LoadAll( connection ) ;
Console.WriteLine( rows.Count ) ;- Quicker way to see have many rows in a table.
ProductsFactory factory = new ProductsFactory();
int rowcount = factory.GetRowCount( connection ) ;- Load up a specific row from a table using the primary key.
int id = 17 ;
ProductsFactory factory = new ProductsFactory();
Products product = factory.LoadByPrimaryKey( connection, id ) ;- Load up a set of rows from a table based on custom search criteria.
bool discontinued = true ;
ProductsFactory factory = new ProductsFactory();
List rows = factory.LoadByDiscontinued( connection, discontinued ) ;<Tables>
<Table Class="Products" >
<Methods>
<Method Name="LoadByDiscontinued" where="LoadByDiscontinued.sql">
<Parameters>
<Parameter Name="discontinued" Type="bool?" />
</Parameters>
</Method>
</Methods>
</Table>
<Tables>where discontinued = @discontinued order by productname- Get a row’s parent row.
EmployeesFactory factory = new EmployeesFactory();
// robert - the pleb
int? employeeId = 7;
Employees robert = factory.LoadByPrimaryKey( connection, employeeId);
Assert.AreEqual( robert.FirstName, "Robert" ) ;
// robert reports to steven
Employees steven = robert.parentEmployeesByReportsTo( connection ) ;
Assert.AreEqual(steven.FirstName, "Steven");
// steven reports to andrew
Employees andy = steven.parentEmployeesByReportsTo( connection);
Assert.AreEqual(andy.FirstName, "Andrew");
// andrew is the big cheese
Employees nevilleNobody = andy.parentEmployeesByReportsTo(connection);
Assert.IsNull(nevilleNobody);
- Get a row’s children rows.
OrdersFactory factory = new OrdersFactory();
int orderId = 10289 ;
// get an order
Orders order = factory.LoadByPrimaryKey( connection, orderId);
// the customer for this order
Customers customer = order.parentCustomersByCustomerID( connection) ;
// all the orders of this customer
List orderList = customer.childOrdersByCustomerID( connection ) ;
- Load up from a view.
Alphabetical_list_of_productsFactory factory = new Alphabetical_list_of_productsFactory();
List rows = factory.LoadAll( connection ) ;- Load up from a custom query.
CustomerOrderFactory factory = new CustomerOrderFactory();
List rows = factory.LoadByRegion( connection, "Quebec" );
List rows2 = factory.LoadByCustomer( connection, "quick" ) ;
<Queries>
<Query Class="CustomerOrder" CodeFile="CustomerOrder.cs" Select="CustomerOrder.sql" >
<Methods>
<Method Name="LoadByRegion" Where="LoadByRegion.sql">
<Parameters>
<Parameter Name="region" Type="string" />
</Parameters>
</Method>
</Methods>
<Method Name="LoadByCustomer" Where="LoadByCustomer.sql">
<Parameters>
<Parameter Name="customerId" Type="string" />
</Parameters>
</Method>
</Query>
</Queries>
The CustomerOrder.sql file contains
select * from customers c, orders o
whilst the LoadByCustomer method has the following criteria in the config sql file reference
where c.customerid = o.customerid
and c.customerid = @customerid
order by c.companyname, o.orderdate
and the LoadByRegion method’s where clause is
where c.customerid = o.customerid
and isnull(c.region, '') = isnull(@region, '')
order by c.companyname, o.orderdate
- Insert a new row
OrdersFactory factory = new OrdersFactory();
Orders order = new Orders() ; // create a new row in memory
order.CustomerID = 123;
order.EmployeeID = 7 ;
order = factory.Save( order, connection ) ; // does the insert- Update an existing row
OrdersFactory factory = new OrdersFactory();
int orderId = 10289 ;
Orders order = factory.LoadByPrimaryKey( connection, orderId); // get an order
order.ShipPostalCode = "2565"
order = factory.Save( order, connection ) ; // does the update- Insert then update a row
OrdersFactory factory = new OrdersFactory();
Orders order = new Orders() ; // create a new row in memory
order.CustomerID = 123;
order.EmployeeID = 7 ;
order = factory.Save( order, connection ) ; // does the insert
order.ShipPostalCode = "2565"
order = factory.Save( order, connection ) ; // does the update
- Delete an existing row
OrdersFactory factory = new OrdersFactory();
int orderId = 10289 ;
Orders order = factory.LoadByPrimaryKey( connection, orderId); // get an order
order.MarkForDelete = true ;
factory.Save( order, connection ) ; // does the delete
- Negative use case - get a concurrency error.
TestTableFactory factory = new TestTableFactory();
TestTable alice = factory.LoadByPrimaryKey( connection, 12345 ); // get a row
TestTable bob = factory.LoadByPrimaryKey( connection, 12345 ); // get it again
alice.update_date = DateTime.Now ;
bob.update_date = DateTime.Now ;
alice = factory.Save( alice, connection ) ; // does the save ok
bob = factory.Save( bob, connection ) ; // will throw a concurrency exception
- Negative use case - get a save error when saving a stale object
OrdersFactory factory = new OrdersFactory();
int orderId = 10289 ;
Orders order = factory.LoadByPrimaryKey( connection, orderId); // get an order
order.ShipPostalCode = "2565"
factory.Save( order, connection ) ; // does the update - ok
order.ShipPostalCode = "2000"
factory.Save( order, connection ) ; // will throw a stale row exception
- Many users simultaneously inserting rows into a table.
In the first version of this use case, the inserts fail, as the code and sql that determine the next primary key value are not protected against concurrent threads. The test fails with the error “Violation of PRIMARY KEY constraint ‘PK_TestTable3′. Cannot insert duplicate key”.
protected void InsertLotsOfRowsInParallel()
{
using ( MyThreadPoolManager tpm = new MyThreadPoolManager( 50, 5000 ))
{
for (int i = 1; i <= rowsToInsert; i++)
tpm.Queue( InsertOneRow() ) ;
tpm.WaitUntilAllFinished() ;
}
}
// every invocation of this method runs in its own thread
protected void InsertOneRow()
{
using (TransactionScope transaction =
new TransactionScope(TransactionScopeOption.RequiresNew))
{
using (SqlConnection connection = new SqlConnection( _connectionString ) )
{
connection.Open();
TestTable3Factory factory = new TestTable3Factory();
TestTable3 obj = new TestTable3();
// BAD: get the next available ID = max(ID) + 1
obj.ID = select max(ID) + 1 from the table
obj.update_date = DateTime.Now;
// insert the row - this will fail with PRIMARY KEY constraint violation
obj = factory.Save(obj, connection);
transaction.Complete(); // commit - wont get here
}
}
}
In the correct version of this use case, the codegen tool has ootb capability to handle this common scenario. The tool will escalate the lock around the next primary key value and serialise access to the table, giving everyone a warm fuzzy feeling.
// every invocation of this method runs in its own thread
protected void InsertOneRow()
{
using (TransactionScope transaction =
new TransactionScope(TransactionScopeOption.RequiresNew))
{
using (SqlConnection connection = new SqlConnection( _connectionString ) )
{
connection.Open();
TestTable3Factory factory = new TestTable3Factory();
TestTable3 obj = new TestTable3();
// GOOD: the tool will get the next available ID, the safe way
obj.ID = factory.GetNextId( connection, TestTable3.Column_ID, true );
obj.update_date = DateTime.Now;
// insert the row - now it works
obj = factory.Save(obj, connection);
transaction.Complete(); // commit - aha
}
}
}
- Here are a few .NET Reflector screen dumps of the DAL layer that the tool generates for Northwind.
- One of the databases that I have recently codegened is the popular DotNetNuke database.
I recommend .NET Reflector to inspect the generated code.
Contact Buildmasters if you would like a DAL generated for your database.
- This is the to do list for the tool
- Make sure it compiles and works under Mono and Debian Linux.
- Make sure it compiles and runs under VS2008.
- Extend to target SqlServer 2008, MySql, Oracle databases.
- Extend to handle stored procedures.
So far I have used the tool against 13 corporate, vendor and public databases, and it has my made my programming experience easier, faster, and more reliable.
I do not think about the DAL layer at all now - it just works.
Comments
2 Responses to “The search for a C# DAL code generator that actually works. Part 3: the solution.”
Got something to say?


Codegen now works on Windows Server 2008 / .NET 3.5 / Visual Studio 2008 / Sql Server 2008 platform.
- tubby
Hello,
I would like to know how can I get a copy of your DAL code generator.
I’ve a lots of problems loading tables from a ODBC database into my dataset.
My PC freezes for 30+ minutes for each table (and sometimes for each step). I need another option and I think this tool can help me with that problem.
Can I download the tool from this site?
Manu.