Introduction
Well designed object models increase reliability and code reuse. We all know that. But building and maintaining them can be a pain, especially in a changing environment. In this three-part series, we'll look at how rolling your own code generators can speed your development and take the fear and bite out of rapid refactoring.
In this, the third and final part in our three-part series, we'll design and build a simple desktop application that performs some rudimentary and not so rudimentary functions. The focus will be on structuring the application in such a way that we leverage our generated entity/factory code from the previous article for easier implementation, more stable code, and simple and reliable refactoring.
For this article, we'll be using a snapshot of the Northwind sample database on Microsoft SQL Server. I used DTS to copy it as "Northwind2". The example code is hard-coded to this connection string.
If you read the previous articles prior to the publication of this one, you may wish to go back and re-get the EntityCodeGen application and source. There were some errors and holes in the initial code included in the article.
Basic Project Layout
|
This is a desktop application for ease of the demo, but it closely simulates a web or distributed application - the code structure is the same. If you look at the two projects, the separation should be self-evident. The generated code and requisite interfaces are packaged in a separate project which will be compiled into a separate DLL. I chose the Customer -> Orders relationship for this example to keep the code base small. You will notice the entity and factory classes, as well as the IPrimaryKey interface and DataAccess classes packaged together. The database connection string is hard-coded into the DataAccess class, if you need to make any changes. Make sure your namespaces match up.
The user interface code is packaged in a separate project, EntityRefactorExample, which will be compiled to an EXE. This could just have easily been a web application. In the references (in the source code), you will notice that the main application includes a reference to the DataBaseEntities DLL/project. You will also find an appropriate using statement in the code.
For this technique to work properly, all your generated code must be held separately and isolated so that it can be regenerated and recompiled with automated build scripts. Our code generator from the previous article would need to be modified to be run from the command line without any user interface interaction, for one. Then it's a matter of writing the proper batch files or ant scripts to regenerate and redistribute this code. All the subclasses and human managed code will live in the EntityRefactorExample project. |
Basic Functionality
Our basic application is shown here. It does a simple lookup based on CustomerID and displays the Customers.CompanyName
and Customers.ContactName
values along with orders for that customer. As a bonus, you can save changes to company name or contact name. Here're the basic calls that make that happen:
void LookupCustomer(string custID)
{
try
{
CustomersEntity ce = custFactory.FindByPrimaryKey(custID);
ArrayList custOrders = new ArrayList(orderFactory.GatherAll());
this.LoadOrderInfo(custOrders);
this.txtCoName.Text = ce.CompanyName;
this.txtContactName.Text = ce.ContactName;
}
catch(Exception ex)
{
MessageBox.Show(this, "Error during lookup: \n"
+ ex.Message, "Lookup Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
...
private void LoadOrderInfo(ArrayList orders)
{
this.dataGrid1.DataSource = orders;
}
The factory classes custFactory
and orderFactory
were declared prior and initialized in the constructor.
custFactory = new CustomersFactory();
orderFactory = new OrdersFactory();
It works pretty good, but not good enough. Our generated factories can't limit what we retrieve. It's either one specific item or everything in the database. To get around this, we're going to subclass both our factories and build on what we already have.
- Create a new class file in the EntityRefactorExample project and call it "
BetterOrdersFactory
".
- Add a
using DataBaseEntities;
statement to the file (and System.Collections
).
- Have the class extend the
OrdersFactory
: public class BetterOrdersFactory : OrdersFactory
.
From here, we create two new methods: one protected
method to allow WHERE
clauses in our SQL and one public
method to search on CustomerID. They will build on our existing methods.
protected string GatherBySimpleWhere(string whereClause)
{
string sql = base.BaseEntitySelect() + " FROM [Orders] base "
+ " WHERE " + whereClause;
return sql;
}
public ICollection GatherByCustomerID(string custID)
{
string sql = GatherBySimpleWhere("CustomerID = '" + custID + "'");
System.Data.DataTable table = DataAccess.ExecuteSqlSelect(sql);
System.Collections.ArrayList lst = new System.Collections.ArrayList();
for (int i = 0; (i < table.Rows.Count); i = i+1)
{
lst.Add(base.EntityFromTableRow(table.Rows[i]));
}
return lst;
}
Here we call to our base object for our methods BaseEntitySelect
and EntityFromTableRow
in our methods. We will then change the factory objects we use and adjust our method calls.
Note: Obviously the dynamic query construction is highly risky, but remember, this is only an example. Do resolve this issue before you use this on a production application.
BetterOrdersFactory orderFactory = new BetterOrdersFactory();
...
ArrayList custOrders =
new ArrayList(orderFactory.GatherByCustomerID(ce.CustomerID));
So now our application actually does what we want it to do:
- Display CompanyName and ContactName of a customer.
- Allow saves of CompanyName and ContactName.
- Display all orders for a customer.
Fantastic.
Ref@#!!*$%toring
You ever go out to lunch on Tuesday thinking your code was done and return only to have the sales people also returning from lunch? With a major customer? Who wants all new functionality? Have you ever had the sales people already have the functionality sold and deliverable? By Friday?
Yeah, me neither.
Changing Requirements
The CEO over at ACME Megacorp has decided that the most important thing to track with customers is their favorite color. That's where the sales and money come from. Over here at Piccolo Softo we're scrambling for every account, so we're going to make sure we deliver what they want, and yesterday since those guys over at Anosoft have been trying to steal this account for months.
The second requirement is a little more complicated. A new business need has arisen to allow multiple customers to be associated with a single order. Our application user interface remains the same, with viewing of orders by customer, but the underlying data is changing quite a bit.
Enable storing Favorite Color
In this case we're adding a column to the Customers table called FavoriteColor
. Run the below SQL script on your Northwind2 database.
ALTER TABLE [Customers]
ADD [FavoriteColor] varchar(50) NULL
That's all well and good. If we run the application after making this change everything still works, so we didn't break anything. But now we have to make the UI changes to support this. Add a new textbox called txtFavColor
. Now it has to be wired for population and save. This is the step where we fire up our code generator and regenerate the classes for CustomersEntity
and CustomersFactory
.
- Use the EntityCodeGen application to regenerate the code for the Customers table.
- Replace the code in your DatabaseEntities project and recompile the DLL.
At this point your CustomersEntity
should have the new column exposed as a property. Next you need to add code to wire this up to the appropriate methods in the form code:
void LookupCustomer(string custID)
{
try
{
CustomersEntity ce = custFactory.FindByPrimaryKey(custID);
ArrayList custOrders = new
ArrayList(orderFactory.GatherByCustomerID(ce.CustomerID));
this.LoadOrderInfo(custOrders);
this.txtCoName.Text = ce.CompanyName;
this.txtContactName.Text = ce.ContactName;
this.txtFavColor.Text = ce.FavoriteColor;
}
catch(Exception ex)
{
MessageBox.Show(this, "Error during lookup: \n" +
ex.Message, "Lookup Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void btnSave_Click(object sender, System.EventArgs e)
{
try
{
CustomersEntity ce =
custFactory.FindByPrimaryKey(this.cmboCustomers.Text);
ce.CompanyName = this.txtCoName.Text;
ce.ContactName = this.txtContactName.Text;
ce.FavoriteColor = this.txtFavColor.Text;
custFactory.Save(ce);
}
catch(Exception ex)
{
MessageBox.Show(this, "Error during save: \n" +
ex.Message, "Save Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
Yes, that is just two lines of code. Since everything else is generated all the other code is handled transparently. If we have a proper command line interface and batch build/deploy process on our code generator, it wouldn't even be this complicated.
Redesigning the Customers -> Orders relationship
Our requirement changes this relationship from a one-to-many to a many-to-many. We'll be adding a linking table and resetting the relationships as shown below:
| |
One to many Customers-Orders relationship | Many to many Customers-Orders relationship |
Our example is primarily dealing with middle tier-forward, so I'll let our big DBA in the sky give you the SQL to run in order to make these changes.
ALTER TABLE Orders
DROP CONSTRAINT FK_Orders_Customers
GO
CREATE TABLE [dbo].[OrdersCustomers] (
[OrderID] [int] NOT NULL ,
[CustomerID] [nchar] (5) NOT NULL
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[OrdersCustomers] ADD
CONSTRAINT [PK_OrdersCustomers] PRIMARY KEY CLUSTERED
(
[OrderID],
[CustomerID]
) ON [PRIMARY],
CONSTRAINT [FK_OrdersCustomers_Customers] FOREIGN KEY
(
[CustomerID]
) REFERENCES [dbo].[Customers] (
[CustomerID]
),
CONSTRAINT [FK_OrdersCustomers_Orders] FOREIGN KEY
(
[OrderID]
) REFERENCES [dbo].[Orders] (
[OrderID]
)
GO
INSERT INTO OrdersCustomers(OrderID, CustomerID)
SELECT OrderID, CustomerID FROM Orders
GO
DROP INDEX [Orders].[CustomerID]
DROP INDEX [Orders].[CustomersOrders]
ALTER TABLE Orders
DROP COLUMN CustomerID
GO
If you run the application at this point, it will no longer work. The table structures have changed and we're trying to look for columns that aren't there. Solution: re-run the EntityCodeGen application to rebuilt our objects and adjust the code in BetterOrdersFactory
. In this case, the Orders table is the basis for our regeneration. Create your classes and recompile the DatabaseEntities project as above.
Since we are now joining two tables to get this information, we must change the plumbing behind BetterOrdersFactory.GatherByCustomerID()
. We'll create a new routine to build the linking SQL and call that.
protected string CustomerOrdersGatherSQL(string custID)
{
string sql = base.BaseEntitySelect()
+ " FROM [Orders] base JOIN [OrdersCustomers] link "
+ " ON base.OrderID = link.OrderID "
+ " WHERE link.CustomerID='" + custID + "'";
return sql;
}
public ICollection GatherByCustomerID(string custID)
{
string sql = this.CustomerOrdersGatherSQL(custID);
...
As you can see, our public method signature remains the same, so no changes are required at the presentation layer. The changes are isolated to the class BetterOrdersFactory
.
Huston, we have achieved refactor. Your application will now perform as advertised and, as promised, there's still time for a triple tall Americano.
Conclusion
Entity code generation is a powerful technique and I use some variant of it in every data-driven application I design and code nowadays. It is so efficient and reliable, in fact, that even when I'm working on extending an existing application that doesn't conform to this model, I will implement it before adding new features (sometimes the code has to run side-by-side with existing code). I have found it saves between 30%-70% of the time to do the normal recode/extend life cycle for an application that has had a sorted past life.
To implement this in real world takes some discipline. Design choices you make from day 1 on can make it easier or harder for this technique to work. The Customers table in Northwind, for example, is problematic because the primary key is not auto-generated. As a general rule, I use Identity values or GUIDs with NewID()
for the default value of the primary key field. An application that follows your rules/conventions tightly enough to allow generated code to be 100% reliable will have a staggering effect on developer productivity. As an example, one application I developed consisted of over 120,000 lines of code being managed by a single developer. And with a high level of quality and rapid feature enhancement/addition.
This is but one more tool to put in your toolbox. Powerful alone, it achieves even greater synergies when used in concert with automated unit testing and batch build/deploy scripts. Plug this in to an Agile environment with a small, smart team, and your company can slash big dollars and months of development from even big, complex projects.
And always keep an eye out for places to automate your coding and environment. Do it once and hold your nose. Do it twice, bite your tongue. But if you do it thrice, automate.