![]() |
Development Lifecycle »
Code Generation »
General
Intermediate
License: The Code Project Open License (CPOL)
netTierGeneratorBy Dmitry ZubrilinA 3-tier application framework and code generation tool - the way for rapid and effective development. |
C#, XML.NET 2.0, .NET 3.0, .NET 3.5, Architect, Dev
|
|
Advanced Search |
|
|
|
||||||||||||||||
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.
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:
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.).
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.
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:
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:
string applicationName = CustomSettings.Current.ApplicationName;
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:
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");
}
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:
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.
I use some strict rules that I follow during database development:
There are several different problems which must be solved at this tier.
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.
private static readonly IAgreementServiceDal dal =
DalManager.CreateInstance("Economy.AgreementServiceDal") as IAgreementServiceDal;
Afterwards, a regular BLL method obtaining some data looks like following sample:
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:
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();
}
}
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.
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;
}
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:
A simple XML declaration will look like following sample:
<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:
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).
This node allows declaring additional using(s) for service classes so that we can use types from a declared namespace.
This node allows using types declared in another XML declaration(s) (e.g., in this sample, we can reference to a model ImageInfo).
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.
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.
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.
It is mostly the same as SelectMethod except one difference: these methods are not aimed to return any value.
Here, I will show how to use this tool in the best way. To achieve this, I will use a sample Windows application.
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.
sys.dm_exec_query_stats). Use database for its direct goals – store data, and maintain data indexes and data integrity.uniqueidentifier columns for table primary keys because it is very helpful for many reasons (e.g., maintaining database replications etc.).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.
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.
ServiceFacade class).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.
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.
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).
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;
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:
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);
}
}
Let us imagine that we need to customize some code implementation. There are several different code modifications that can be applied.
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:
DeleteGoodInfoByID from the generated file to a user specific file and apply the code modifications.BllAccess to false in the DeleteMethod of GoodInfo, and regenerate the code from a good XML declaration file.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:
<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.
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:
| You must Sign In to use this message board. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 30 Nov 2008 Editor: Smitha Vijayan |
Copyright 2008 by Dmitry Zubrilin Everything else Copyright © CodeProject, 1999-2009 Web10 | Advertise on the Code Project |