Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / XML

netTierGenerator

4.81/5 (20 votes)
30 Nov 2008CPOL14 min read 68.9K   2.8K  
A 3-tier application framework and code generation tool - the way for rapid and effective development.

Introduction

There are a lot of different ORMs and code generator utilities (e.g., NHibernate, netTiers, Entity Framework etc.). Some of them are based on template driven engines while others are based on solution frameworks. The utility that I would like to present in this article is based on my own solution framework. So it is not just an object relational mapping tool. It rather focuses on presenting a good development environment for any project.

Solution framework

In a few words, my solution framework can be described as classic 3-layered framework. It consists of a Data Access Layer (DAL), Business Layer (BLL), and some rules for the Presentation Layer (GUI). This framework follows the common best practices published by Microsoft in the article Application Architecture for .NET. Graphically, it can be displayed as follows:

Image 1

But from my point of view, the greatest advantage of this utility is the presence of an intermediate meta layer between the code generation utility and the backend data storage (I will call it MetaLayer). This layer is a set of XML documents describing the backed data storage structure, and gives developers an easy way to extend the basic functionality (declaring new business entity classes, new data discovery methods etc.).

Solution framework layers

The most important term in the solution is a “Service”. The solution operates these services like vertical bricks which give the ability to work with a concrete functional piece of the system through all layers (for example, for a dictionary of countries, there will be CountryInfo DTO object, and CountryServiceDAL, ICountryServiceDAL, and CountryService in the BLL). It is important that each service is implemented as a stateless class. The layers are constructed in such a way that the DAL service methods should be called only from the correspondent business logic services. Data access layers are implemented as a set of Data Providers which are accessed via the Data Factory. This allows obtaining a perfect abstraction from the concrete DAL implementation. All layers are implemented as separate projects (assemblies). So, a typical solution will contain at least the following projects: Common, Configuration management, MetaLayer, Business Entities Model, Data Access Layer Interfaces, Data Access Layer implementation, DAL Factory, and Business logic.

Image 2

Common

This is the place where all common code is located (application wide lookups, utility classes etc.). There are some important features which are tightly incorporated into the application solution and the code generator utility:

  1. Application wide lookups can be used for mapping in business entities;
  2. Business entities validation engine and modification history tracking engine are located here.

Configuration management

This is an implementation of the configuration management through “*.config” files. I had described its detailed implementation in my Configuration Management article some time ago. The main benefit of this project is a functionality that allows obtaining configuration values using a very easy code:

C#
string applicationName = CustomSettings.Current.ApplicationName;

Business Entity Model

A set of data transfer objects is implemented as a separate project. Besides being simple containers for backend data, DTO objects have the following functionality: data validation at application level, IClonable, IEquatable<>. The validation functionality is borrowed from the netTiers project. Its core is implemented as a set of standalone static methods. And each DTO class uses them in the following way:

C#
private static void AddDatabaseChemaRules()
{
  GoodInfoModel.VALIDATION_RULES.AddRule(CommonRules.NotNull, "Name");
  GoodInfoModel.VALIDATION_RULES.AddRule(CommonRules.StringMaxLength, 
                new CommonRules.MaxLengthRuleArgs("Name", 50));
  GoodInfoModel.VALIDATION_RULES.AddRule(CommonRules.NotNull, "Cost");
  GoodInfoModel.VALIDATION_RULES.AddRule(CommonRules.NotNull, "Quantity");
}

Data Access Layer

There are different ways to present data for application needs. One of them is used by Microsoft in ADO.NET, which is based on mapping data on the table level by means of DataTables and DataAdapters. Another way is used by different ORMs like NHibernate, netTiers etc. This is based on mapping data on the table row level. The suggested solution framework maps data like many another ORMs on the table row level. The implementation of DAL can be decoupled as:

  1. Set of DAL Services which implement a concrete functionality.
  2. Set of interface contracts which cover the set of DAL objects.
  3. DAL Factory which creates certain instances of DAL Service classes at run-time.

The actual implementation of the concrete DAL functionality can be different. I have used an approach from the Data Access Application Block v1. You can see the implementation in the attached source code.

Rules for backend databases

I use some strict rules that I follow during database development:

  1. Naming convention for database objects:
    1. Tables – tblNamespace.Essence (e.g., tblAdministration.User, tblAdministration.Role, tblDictionary.Country).
    2. Views – vwNamespace.Essence (e.g. vwDictionary.Country), where tbl/vw prefixes mean table/view, “Administration/Dictionary” means something like a namespace, “User/Role/Country” suffixes mean object name.
  2. Use the backend database as data storage only, do not perform any business activity there. In other words, avoid using triggers and Stored Procedures for business goals. Use database only to maintain data, indexes, and data integrity.
  3. Use GUIDs as primary keys (do not use integers with identity).

Business Layer

There are several different problems which must be solved at this tier.

  1. The most important things are the implementation of session management and specifically transaction management.
  2. The actual problem is the ability to cache the gathered data from the backend database (or wherever else).
  3. It will be very useful if a regular developer will be prevented from making common errors.

The problem with session and transaction management is solved in a way used in many other ORMs. A session and transaction class instance is maintained at the business layer in a thread static manner. So, it is not needed to maintain session between method calls. Also, there are a few strict rules to work with session and transactions at the BLL only. Caching is implemented based on MS Enterprise Library Caching as a standalone service. Cache implementation heavily uses anonymous methods – a new feature for .NET Framework 2.0. So, it is very easy to implement caching data.

I have implemented the usage of the DAL tier in the BLL level in a way that MS suggests in their public project PetShop 4. BLL classes use IDAL interfaces as internal static members, and use the DAL Factory to instantiate concrete implementations.

C#
private static readonly IAgreementServiceDal dal = 
  DalManager.CreateInstance("Economy.AgreementServiceDal") as IAgreementServiceDal;

Afterwards, a regular BLL method obtaining some data looks like following sample:

C#
public AgreementInfoModel GetAgreementInfoById(Guid id)
{
    string key = String.Concat(AgreementService.AGREEMENT_BY_ID, id.ToString());
    return CacheService.GetData<agreementinfomodel>(
            key,
            AgreementService.AGREEMENT_BY_ID_SINC_KEY,
            TimeSpan.FromSeconds(AgreementService.AGREEMENT_BY_ID_CACHE_INTERVAL),
            delegate
            {
                AgreementInfoModel agreement = null;
                using (Session session = base.OpenSession())
                {
                    agreement = dal.GetAgreementInfoById(session.Current, id);
                    if (agreement != null)
                    {
                        agreement.CurrentAmount = 
                          dal.GetAgreementTransferAmountByAgreementId(
                          session.Current, agreement.Id, DateTime.Today);
                    }
                }
                return agreement;
            }
        );
}

A regular BLL method performing some activity looks like following sample:

C#
public void DeleteAgreementInfoById(AgreementToInfoModel agreementTO)
{
    using (Session session = base.OpenSession())
    using (Transaction tx = session.BeginTransaction())        
    {
        if (agreement.ImageId != Guid.Empty)
        {
            ServiceFacade.ImageService.DeleteImageInfoById(agreement.ImageId);
        }
        dal.DeleteAgreementInfoById(session.Current, agreement.Id);
        tx.Commit();
    }
}

GUI usage of tiers below

There is a Service façade which aggregates all BLL services into a single point. This Service façade makes method calls from the GUI to the tiers below very easily.

C#
protected void btnSave_Click(object sender, EventArgs e)
{
    if (Page.IsValid)
    {
        BranchInfoModel item = this.pc.GetObject() as BranchInfoModel;
        ServiceFacade.BranchService.SaveBranch(item);

        this.ShowListPage();
    }
}
public override object GetObject()
{
    BranchInfoModel item = ServiceFacade.BranchService.GetBranchInfoById(this.ItemId);
    if (item == null)
    {
        item = new BranchInfoModel();
    }
    item.Name = this.tbName.Text;
    item.Email = this.etbEmail.Text;
    return item;
}

NetTierGenerator code generator utility

The solution framework above gives the perfect environment for code generation utilities. The suggested code generator handles DTO models, DAL (+ IDAL), and BLL layers. Also, it has an intermediate level between the backend database structure and the output code. This allows creating additional methods with database calls at this intermediate level.

There is another important feature of this utility. It generates two physical files for each class (one for its generated content and one for developer needs; both these files at each layer contain a declaration for single C# classes as partial items):

The suggested code generator utility solves the following tasks:

  1. It allows placing the generated content into target namespace(s).
  2. It maps database tables/views to its own intermediate declaration structure.
  3. It maps each intermediate XML declaration file to its own application service.
  4. Each intermediate XML declaration file can contain mappings to several database tables/views (this allows to tie them into a single service).
  5. It allows to easily map database table columns to a C# enumerator.
  6. It allows declaring additional methods which interact with the backend database.
  7. It allows manipulating code generation at each layer so that it is pretty easy to overwrite the generated content to a custom code.
  8. It has a GUI for creation of XML declarations from the backend database.
  9. It has functionality for obtaining paged data (here, I’m using one of the methods described in this article: http://www.codeproject.com/KB/aspnet/PagingLarge.aspx).

A simple XML declaration will look like following sample:

XML
<TierModel Namespace="Economy" ServiceName="City">
    <Declare Type="Solution.Common.Economy.BranchConditionEnum" />
    <Declare Type="Solution.Common.Economy.PaymentTypeEnum" />

    <Include Path="Economy\Image.xml" Type="ImageInfo" /> 
    
    <ItemModel DbTable="tblEconomy_City" 
             ClassName="CityInfo" 
             Caching="True" Parent="">
        <Comment />
        <KeyProperty NeedToGenerate="true" ReadOnly="False">
            <Comment />
            <CSharp CSharpName="id" CSharpType="Guid" />
            <Db DbName="rowguid" DbType="uniqueidentifier" 
                     IsNullable="False" Length="16" />
        </KeyProperty>
        <Property ReadOnly="False">
            <Comment />
            <CSharp CSharpName="BranchId" CSharpType="Guid" />
            <Db DbName="BranchId" DbType="uniqueidentifier" 
                      IsNullable="False" Length="16" />
        </Property>
        <Property ReadOnly="False">
            <Comment />
            <CSharp CSharpName="Name" CSharpType="string" Length="100" />
            <Db DbName="Name" DbType="nvarchar" 
                     IsNullable="False" Length="100" />
        </Property>
        <SelectMethod NeedToCreate="True" DalAccess="True" BllAccess="True" />
        <InsertMethod NeedToCreate="True" DalAccess="True" BllAccess="True" />
        <UpdateMethod NeedToCreate="True" DalAccess="True" BllAccess="True" />
        <DeleteMethod NeedToCreate="True" DalAccess="True" BllAccess="False" />
    </ItemModel>
    
    <ListItemModel DbView="vwEconomy_City" ClassName="CityListItem" Parent="">
        <Comment />
        <KeyProperty>
            <Comment />
            <CSharp CSharpName="id" CSharpType="Guid" />
            <Db DbName="rowguid" DbType="uniqueidentifier" 
                      IsNullable="False" Length="16" />
        </KeyProperty>
        <Property>
            <Comment />
            <CSharp CSharpName="BranchId" CSharpType="Guid" />
            <Db DbName="BranchId" DbType="uniqueidentifier" 
                     IsNullable="False" Length="16" />
        </Property>
        <Property>
            <Comment />
            <CSharp CSharpName="Name" CSharpType="string" Length="100" />
            <Db DbName="Name" DbType="nvarchar" 
                    IsNullable="False" Length="100" />
        </Property>
        <Property ReadOnly="False">
            <Comment />
            <CSharp CSharpName="BranchName" 
                     CSharpType="string" Length="100" />
            <Db DbName="BranchName" DbType="nvarchar" 
                     IsNullable="False" Length="100" />
        </Property>
    </ListItemModel>
    
    <SelectMethod Name="GetCitiesByBranchId" 
               DalAccess="True" BllAccess="True">
        <Comment />
        <Return ReturnType="IList" Type="CityInfo">
            <Comment />
        </Return>
        <Property>
            <Comment />
            <CSharp CSharpName="BranchId" CSharpType="Guid" />
            <Db DbName="BranchId" DbType="uniqueidentifier" 
                    IsNullable="False" Length="16" />
        </Property>
        <Sql>
            <Query><![CDATA[SELECT * FROM tblEconomy_City 
                         WHERE BranchId = @BranchId]]></Query>
        </Sql>
    </SelectMethod>
    <SelectMethod Name="GetListPage" DalAccess="True" BllAccess="True">
        <Comment />
        <Return ReturnType="ListPage" Type="CityListItem">
            <Comment />
        </Return>
        <Sql>
            <Query><![CDATA[SELECT * FROM vwEconomy_City]]></Query>
        </Sql>
    </SelectMethod>

    <UpdateMethod Name="InsertCityImage" 
              DalAccess="True" BllAccess="False">
        <Comment></Comment>
        <Property>
            <Comment />
            <CSharp CSharpName="CityId" CSharpType="Guid" />
            <Db DbName="CityId" DbType="uniqueidentifier" 
                   IsNullable="False" Length="16" />
        </Property>
        <Property>
            <Comment />
            <CSharp CSharpName="ImageId" CSharpType="Guid" />
            <Db DbName="ImageId" DbType="uniqueidentifier" 
                      IsNullable="False" Length="16" />
        </Property>
        <Sql>
            <Query><![CDATA[INSERT INTO tblEconomy_CityImages 
                     (CityId, ImageId) VALUES (@CityId, @ImageId)]]></Query>
        </Sql>
    </UpdateMethod>
</TierModel>

As you can see, it is pretty straightforward. And, as for me, it is very attractive that the developer can determine the SQL statement at this point. Because, each database engine has its own strong and weak points, and it is not a good idea to try to write/generate SQL code for all database engines in the same unified way (e.g., MS SQL server contains a number of unique SQL statements like EXISTS, recursive CTE, HierarcyId etc.).

Here is a description of the declaration elements of the above XML in detail:

TierModel

This is a root node which determines the service name and its placement at each tier (the sample above will be placed in the namespace Economy at every layer with the name CityService).

Declare

This node allows declaring additional using(s) for service classes so that we can use types from a declared namespace.

Include

This node allows using types declared in another XML declaration(s) (e.g., in this sample, we can reference to a model ImageInfo).

ItemModel

This is a base node type for the code generator utility at whole. It maps a database table to a single DTO class. It allows declaring the key property which will be used in the generated CRUD operations. It allows mapping each table column to its own DTO class property, and also allows pointing details for the generated validation code.

ListItemModel

This node is aimed to declare DTO classes for lists (the main difference from the item model is that the list item model can contain fields from other essences; for example, CityListItem can contain a BranchName field). Also, there is no CRUD operation for them, but their key properties are used for maintaining strict ordering for lists at run-time.

SelectMethod

This node allows declaring custom methods. These methods can return either C# types or types declared in intermediate XML. Also, it contains properties which will further be propagated to method signature and SQL statement parameters. Also, we can bring a custom implementation for a method at every layer by manipulating the DalAccess/BLLAccess attributes. Also, it contains a SQL node where we can set up a SQL statement. This is the point where potentially we can split the implementation for different backend databases, if needed.

UpdateMethod

It is mostly the same as SelectMethod except one difference: these methods are not aimed to return any value.

How to use NetTierGenerator

Here, I will show how to use this tool in the best way. To achieve this, I will use a sample Windows application.

MS SQL Server backend database

I will use a simple database structure that contains a few tables. All of them are compatible to a small set of rules. The tables from the “Administration” namespace will not be used in the GUI application; they are included only to show some useful tricks of the NetTierGenerator and the suggested application framework.

  1. Table/view name must reflect the namespace and item name.
  2. Avoid using database triggers and Stored Procedures (just in case SQL Server is capable of store execution plans for most of the requested queries, see sys.dm_exec_query_stats). Use database for its direct goals – store data, and maintain data indexes and data integrity.
  3. Always try to use uniqueidentifier columns for table primary keys because it is very helpful for many reasons (e.g., maintaining database replications etc.).

First steps with NetTierGenerator

NetTierGenerator has two executables: the first one is the GUI application, and the second one is the console application. The GUI application is aimed to allow quick creation of new definitions from the backend database. The console application is aimed to quickly apply modifications in definitions.

  • First, we need to define a certain place for the NetTierGenerator binaries and adjust its settings. Here is the solution's physical folder structure with a folder “Tools” that contains the NetTierGenerator binaries:

Image 3

  • Modify the “App.config” file of NetTierGenerator to reflect your current database settings.
  • Next, we need to configure NetTierGenerator. It can be done by modifying “TierGeneratorSettings.xml” or by using “NetTierGenerator.WinUI.exe”.
  • We now need to register both these executables as external tools in the Visual Studio IDE, and assign keyboard shortcuts to them.

Image 4

Map the backend database structure to your solution

Now, we are ready to create the XML mappings. We just need just to open the solution in the VS IDE, and then start NetTierGeneratorWinGUI.

  1. Select “tblStore_Good” in the “Database tables” combobox. Then, the application will automatically adjust the “Namespace” and “Service Name” settings for the current model. Then, go to the “List Item” tab and select vwStoore_Good. Click the “Generate XML only” button.
  2. NetTierGenerator generates an XML declaration into the “TierModel” folder.
  3. Now, open this XML declaration in the VS IDE and use the console version of NetTierGenerator. Also, at this time, you can add any additional methods you want.
  4. The system will generate:
    1. “GoodInfo”, “GoodListItem” into the Sample.Model project.
    2. IGoodServiceDal into the Sample.IDAL project.
    3. GoodServiceDAL into the Sample.MSSqlDal project.
    4. GoodService into the Sample.BusinessLogic project (it will also modify the ServiceFacade class).

Image 5

The system will generate two files per each class. One for NetTierGenerator, which needs these files and will be regenerated each time. And, the second one for user defined code.

How to use previously generated stuff

There is a single point that allows access to all methods in the BLL – ServiceFacade. The developer does not need to create instances of BLL services in the presentation layer.

Lists support

One of the most common goals is displaying lists. The sample application uses the DataGridView control in Virtual mode to display data in the grid with vertical scrolling enabled. It receives a small portion of data each time. Here is the code that performs this task from the Presentation Layer (sorting and filtering could be easily adjusted at this point too).

C#
this.currentGoodListQuery = new ListQuery();
this.currentGoodListQuery.RowsPerPage = 
     this.dgvOrderList.DisplayedRowCount(false);
this.currentGoodListQuery.FirstRowIndex = 
     this.dgvOrderList.FirstDisplayedScrollingRowIndex;

this.currentGoodListPage = 
  ServiceFacade.GoodService.GetListPage(this.currentGoodListQuery);
this.dgvOrderList.RowCount = this.currentGoodListPage.TotalRowCount;

Raw level access and CRUD

This application framework gives a very easy way of using CRUD operations. Here is the sample code that populates a DTO instance with input data and stores it into the backend database:

C#
private void SaveInfoModel()
{
    if (this.goodInfoModel == null)
    {
        this.goodInfoModel = new GoodInfoModel();
    }

    this.goodInfoModel.Name = this.tbName.Text;
    this.goodInfoModel.Cost = FormatHelper.ParseDecimal(this.tbCost.Text);
    this.goodInfoModel.Quantity = FormatHelper.ParseInt32(this.tbQuantity.Text);

    if (this.goodInfoModel.ID == Guid.Empty)
    {
        ServiceFacade.GoodService.InsertGoodInfo(this.goodInfoModel);
    }
    else
    {
        ServiceFacade.GoodService.UpdateGoodInfo(this.goodInfoModel);
    }
}

How to customize the code

Let us imagine that we need to customize some code implementation. There are several different code modifications that can be applied.

Override the generated code

For example, we need to email someone when an item is removed. In that particular case, we need to extend the implementation of DeleteGoodInfo in the BLL. To perform this task, we need to do the following two steps:

  1. Move DeleteGoodInfoByID from the generated file to a user specific file and apply the code modifications.
  2. Open “Good.xml”, set BllAccess to false in the DeleteMethod of GoodInfo, and regenerate the code from a good XML declaration file.

Add a user defined function

For example, we need a functionality to obtain the total amount of goods that cost less than a specified amount. In this case, we need to define our own method with a custom behavior. This declaration can look like:

XML
<SelectMethod Name="GetGoodAmountByCost">
    <Comment />
    <Return ReturnType="int">
        <Comment />
    </Return>
    <Property>
        <Comment />
        <CSharp CSharpName="Cost" CSharpType="decimal" />
        <Db DbName="Cost" DbType="numeric" 
          IsNullable="False" Length="9" />
    </Property>
    <Sql><Query>
<![CDATA[SELECT COUNT(ID) AS [count] FROM tblStore_Good WHERE Cost <= @Cost]]>
    </Query></Sql>
</SelectMethod>

After you define these methods in XML, you will need to regenerate the code. The DAL, IDAL, and BLL tiers will be modified accordingly, so that you can use this method with the specified parameters right from the Presentation Layer.

Conclusion

At this point, the solution and the code generator above are not aimed at solving all the problems of the application developer. From my point of view, their mission can be described as following:

  1. They help to make an application skeleton in a few minutes. Also, it allows to easily keep all the application layers updated after any change to the underlying data structure.
  2. The solution framework defines certain points for the implementation of business rules, data mining, and the GUI. So, the solution will not contain a mess of these items.
  3. The solution and code generator are constructed together.
  4. There is a meta level (XML declaration) where we can manage the generated code output. In other words, this can be named “meta development”.
  5. It divides the developed application into strict layers. It allows to easily manage database transactions. It defines a strict place for the implementation of business rules so you will not have a mess in your code.
  6. All activity is done at design time, not at run time.
  7. They make application development more manageable, easy, and fun.
  8. They deprive application developers of common errors.
  9. The NetTierGenerator application itself can be adjusted to any specific need.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)