Click here to Skip to main content
15,892,965 members
Articles / Programming Languages / C#
Article

Rapid development and refactoring using code generators - Part 3

Rate me:
Please Sign up or sign in to vote.
3.91/5 (7 votes)
2 Aug 20058 min read 32.7K   257   43   3
Using CodeDOM and SQL Server to build entity and factory classes - Part 3: Packaging your application and subclassing for easy refactoring.

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

Example solution code structure

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

Example application

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:

C#
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.

C#
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.

  1. Create a new class file in the EntityRefactorExample project and call it "BetterOrdersFactory".
  2. Add a using DataBaseEntities; statement to the file (and System.Collections).
  3. 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.

C#
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.

C#
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.

SQL
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.

  1. Use the EntityCodeGen application to regenerate the code for the Customers table.
  2. 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:

C#
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 relationshipMany to many Customers-Orders relationship
One to many Customers-Orders relationshipMany 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.

SQL
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.

C#
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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Architect Milliman
United States United States
I have been involved in professional software development for over 15 years, focusing on distributed applications on both Microsoft and Java platforms.

I also like long walks on the beach and a sense of humor and don't like mean people Wink | ;-)

Comments and Discussions

 
GeneralTypo Pin
DejaVudew12-Aug-05 9:44
DejaVudew12-Aug-05 9:44 
GeneralGreat article series... Pin
Ashley van Gerven3-Aug-05 2:40
Ashley van Gerven3-Aug-05 2:40 
GeneralRe: Great article series... Pin
Chris Cole3-Aug-05 6:06
professionalChris Cole3-Aug-05 6:06 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.