netTierGenerator






4.81/5 (19投票s)
一个三层应用程序框架和代码生成工具——实现快速有效开发的途径。
引言
市面上有许多不同的ORM和代码生成工具(例如,NHibernate、netTiers、Entity Framework等)。有些基于模板驱动引擎,有些则基于解决方案框架。我想在本文中介绍的工具是基于我自己的解决方案框架。因此,它不仅仅是一个对象关系映射工具。它更侧重于为任何项目提供一个良好的开发环境。
解决方案框架
简单来说,我的解决方案框架可以描述为经典的3层框架。它包括一个数据访问层(DAL)、业务逻辑层(BLL)以及一些用于表示层(GUI)的规则。该框架遵循微软在其文章.NET应用程序架构中发布的通用最佳实践。图形化展示如下:
但从我的角度来看,这个工具最大的优势是代码生成工具和后端数据存储之间存在一个中间元层(我称之为MetaLayer)。这一层是一组描述后端数据存储结构的XML文档,它为开发人员提供了一种简单的方式来扩展基本功能(声明新的业务实体类、新的数据发现方法等)。
解决方案框架层
解决方案中最重要的术语是“服务”。该解决方案将这些服务作为垂直构件来操作,使系统能够通过所有层处理特定的功能块(例如,对于国家字典,将有一个CountryInfo
DTO对象,并在BLL中有CountryServiceDAL
、ICountryServiceDAL
和CountryService
)。重要的是,每个服务都实现为无状态类。层的构建方式是,DAL服务方法只能从相应的业务逻辑服务调用。数据访问层实现为一组通过Data Factory访问的数据提供程序。这允许获得对具体DAL实现的完美抽象。所有层都实现为单独的项目(程序集)。因此,一个典型的解决方案至少包含以下项目:Common、Configuration management、MetaLayer、Business Entities Model、Data Access Layer Interfaces、Data Access Layer implementation、DAL Factory和Business logic。
Common
这是所有通用代码所在的地方(应用程序范围的查找、实用类等)。有一些重要的功能紧密集成到应用程序解决方案和代码生成工具中。
- 应用程序范围的查找可用于业务实体中的映射;
- 业务实体验证引擎和修改历史跟踪引擎位于此处。
配置管理
这是通过“*.config”文件实现的配置管理。我很久以前就在我的配置管理文章中详细描述了它的实现。该项目的主要好处是能够通过非常简单的代码获取配置值的功能。
string applicationName = CustomSettings.Current.ApplicationName;
业务实体模型
数据传输对象集实现为一个单独的项目。除了作为后端数据的简单容器之外,DTO对象还具有以下功能:应用程序级别的数据验证、IClonable
、IEquatable<>
。验证功能是从netTiers项目借用的。其核心实现为一组独立的静态方法。并且每个DTO类都以以下方式使用它们:
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");
}
数据访问层
为了满足应用程序的需求,有不同的方式来呈现数据。微软在ADO.NET中使用的一种方式是,通过DataTable和DataAdapter在表级别映射数据。另一种方式被NHibernate、netTiers等不同的ORM使用。这是基于在行级别映射数据。建议的解决方案框架像许多其他ORM一样,在行级别映射数据。DAL的实现可以解耦为:
- 一组实现具体功能的DAL服务。
- 一组覆盖DAL对象的接口契约。
- DAL Factory,它在运行时创建DAL服务类的特定实例。
具体DAL功能的实际实现可以不同。我使用了Data Access Application Block v1的方法。您可以在附加的源代码中看到实现。
后端数据库规则
我在数据库开发过程中遵循一些严格的规则。
- 数据库对象的命名约定
- 表 – tblNamespace.Essence (例如,tblAdministration.User, tblAdministration.Role, tblDictionary.Country)。
- 视图 – vwNamespace.Essence (例如,vwDictionary.Country),其中tbl/vw前缀表示表/视图,“Administration/Dictionary”表示某种命名空间,“User/Role/Country”后缀表示对象名称。
- 仅将后端数据库用作数据存储,不要在那里执行任何业务活动。换句话说,避免使用触发器和存储过程来实现业务目标。仅使用数据库来维护数据、索引和数据完整性。
- 使用GUID作为主键(不要使用带标识的整数)。
业务层
在此层必须解决几个不同的问题。
- 最重要的事情是会话管理和特别是事务管理的实现。
- 实际问题是能够缓存从后端数据库(或任何其他地方)收集的数据。
- 如果能够阻止普通开发人员犯常见错误,那将非常有益。
会话和事务管理的问题是通过许多其他ORM使用的方式解决的。会话和事务类实例以线程静态的方式在业务层中维护。因此,不需要在方法调用之间维护会话。此外,在BLL中处理会话和事务有一些严格的规则。缓存基于MS Enterprise Library Caching作为独立服务实现。缓存实现大量使用匿名方法——.NET Framework 2.0的一项新功能。因此,实现缓存数据非常容易。
我按照MS在其公共项目PetShop 4中建议的方式,在BLL级别实现了DAL层的用法。BLL类使用IDAL
接口作为内部静态成员,并使用DAL Factory来实例化具体的实现。
private static readonly IAgreementServiceDal dal =
DalManager.CreateInstance("Economy.AgreementServiceDal") as IAgreementServiceDal;
之后,一个常规的BLL方法获取数据如下所示:
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;
}
);
}
一个常规的BLL方法执行某些活动如下所示:
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使用下方各层
有一个Service façade,它将所有BLL服务聚合到一个点。这个Service façade使得从GUI到下方各层的调用非常容易。
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代码生成工具
上述解决方案框架为代码生成工具提供了完美的开发环境。建议的代码生成器处理DTO模型、DAL(+ IDAL)和BLL层。此外,它在后端数据库结构和输出代码之间有一个中间级别。这允许在该中间级别创建具有数据库调用的附加方法。
该工具有另一个重要功能。它为每个类生成两个物理文件(一个用于其生成内容,一个用于开发人员的需要;每个层中的这两个文件都包含对单个C#类作为部分项的声明)。
建议的代码生成工具解决了以下任务:
- 它允许将生成的内容放置到目标命名空间中。
- 它将数据库表/视图映射到自己的中间声明结构。
- 它将每个中间XML声明文件映射到自己的应用程序服务。
- 每个中间XML声明文件可以包含到多个数据库表/视图的映射(这允许将它们绑定到一个服务中)。
- 它允许轻松地将数据库表列映射到C#枚举。
- 它允许声明与后端数据库交互的其他方法。
- 它允许在每个层上操作代码生成,因此很容易将生成的内容覆盖为自定义代码。
- 它有一个GUI,用于从后端数据库创建XML声明。
- 它具有获取分页数据的功能(这里,我使用了本文中描述的方法之一:https://codeproject.org.cn/KB/aspnet/PagingLarge.aspx)。
一个简单的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>
如您所见,它非常直接。对我来说,开发者可以在这一点上确定SQL语句,这非常有吸引力。因为,每个数据库引擎都有其自身的优点和缺点,试图以统一的方式为所有数据库引擎编写/生成SQL代码并不是一个好主意(例如,MS SQL Server包含许多唯一的SQL语句,如EXISTS
、递归CTE、HierarcyId等)。
下面是上述XML声明元素的详细描述:
TierModel
这是根节点,它决定了服务名称及其在每个层中的位置(上述示例将在每个层中放置在Economy
命名空间下,名为CityService
)。
Declare
此节点允许声明服务类的其他using语句,以便我们可以使用声明命名空间中的类型。
Include
此节点允许使用声明在另一个XML声明中的类型(例如,在此示例中,我们可以引用ImageInfo
模型)。
ItemModel
这是代码生成器实用程序的基节点类型。它将数据库表映射到单个DTO类。它允许声明将在生成的CRUD操作中使用的键属性。它允许将每个表列映射到其自己的DTO类属性,并允许指向生成的验证代码的详细信息。
ListItemModel
此节点旨在声明列表的DTO类(与item model的主要区别在于,list item model可以包含来自其他实体的字段;例如,CityListItem
可以包含BranchName
字段)。此外,它们没有CRUD操作,但其键属性用于在运行时维护列表的严格排序。
SelectMethod
此节点允许声明自定义方法。这些方法可以返回C#类型或中间XML中声明的类型。此外,它包含将进一步传播到方法签名和SQL语句参数的属性。此外,我们可以通过操作DalAccess
/BLLAccess
属性在每个层上为方法带来自定义实现。此外,它包含一个SQL节点,我们可以在其中设置SQL语句。这是可以根据需要为不同后端数据库拆分实现的地方。
UpdateMethod
它与SelectMethod
基本相同,只有一个区别:这些方法不旨在返回任何值。
如何使用NetTierGenerator
在这里,我将展示如何以最佳方式使用此工具。为此,我将使用一个示例Windows应用程序。
MS SQL Server后端数据库
我将使用一个包含几个表的简单数据库结构。所有这些都符合一小组规则。“Administration
”命名空间中的表不会在GUI应用程序中使用;它们仅包含在内,以展示NetTierGenerator和建议的应用程序框架的一些有用技巧。
- 表/视图名称必须反映命名空间和项目名称。
- 避免使用数据库触发器和存储过程(以防SQL Server能够为大多数请求的查询存储执行计划,请参阅
sys.dm_exec_query_stats
)。数据库仅用于其直接目的——存储数据,并维护数据索引和数据完整性。 - 始终尝试使用
uniqueidentifier
列作为表主键,因为它在许多方面都非常有用(例如,维护数据库复制等)。
NetTierGenerator的初步使用
NetTierGenerator有两个可执行文件:第一个是GUI应用程序,第二个是控制台应用程序。GUI应用程序旨在允许从后端数据库快速创建新定义。控制台应用程序旨在快速应用对定义的修改。
- 首先,我们需要为NetTierGenerator二进制文件定义一个特定位置并调整其设置。这是解决方案的物理文件夹结构,其中有一个“Tools”文件夹,其中包含NetTierGenerator二进制文件。
- 修改NetTierGenerator的“App.config”文件,以反映您当前的数据库设置。
- 接下来,我们需要配置NetTierGenerator。这可以通过修改“TierGeneratorSettings.xml”或使用“NetTierGenerator.WinUI.exe”来完成。
- 现在我们需要将这两个可执行文件注册为Visual Studio IDE中的外部工具,并为它们分配键盘快捷键。
将后端数据库结构映射到您的解决方案
现在,我们准备创建XML映射。我们只需要在VS IDE中打开解决方案,然后启动NetTierGeneratorWinGUI。
- 在“Database tables”组合框中选择“tblStore_Good”。然后,应用程序将自动调整当前模型的“Namespace”和“Service Name”设置。然后,转到“List Item”选项卡并选择vwStoore_Good。单击“Generate XML only”按钮。
- NetTierGenerator将XML声明生成到“TierModel”文件夹中。
- 现在,在VS IDE中打开此XML声明,并使用控制台版本的NetTierGenerator。此外,此时您可以添加任何您想要的附加方法。
- 系统将生成:
- “GoodInfo”、“GoodListItem”到Sample.Model项目。
- IGoodServiceDal到Sample.IDAL项目。
- GoodServiceDAL到Sample.MSSqlDal项目。
- GoodService到Sample.BusinessLogic项目(它还将修改
ServiceFacade
类)。
系统将为每个类生成两个文件。一个供NetTierGenerator使用,它需要这些文件并在每次重新生成时使用。另一个用于用户定义代码。
如何使用先前生成的内容
有一个单一的点允许访问BLL中的所有方法——ServiceFacade
。开发人员不需要在表示层中创建BLL服务的实例。
列表支持
最常见的目标之一是显示列表。示例应用程序使用DataGridView
控件的Virtual
模式来显示网格中的数据,并启用垂直滚动。它每次接收一小部分数据。以下是从表示层执行此任务的代码(在此点上也可以轻松调整排序和过滤)。
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;
原始级别访问和CRUD
此应用程序框架提供了一种非常简单的CRUD操作使用方式。以下是使用输入数据填充DTO实例并将其存储到后端数据库的示例代码。
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);
}
}
如何自定义代码
让我们想象一下,我们需要自定义某些代码实现。可以应用几种不同的代码修改。
覆盖生成的代码
例如,当一个项目被移除时,我们需要发送电子邮件。在这种特定情况下,我们需要扩展BLL中DeleteGoodInfo
的实现。要执行此任务,我们需要执行以下两个步骤:
- 将
DeleteGoodInfoByID
从生成的文件移动到用户特定文件中,并应用代码修改。 - 打开“Good.xml”,在
GoodInfo
的DeleteMethod
中将BllAccess
设置为false
,并从好的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>
在XML中定义这些方法后,您将需要重新生成代码。DAL、IDAL和BLL层将相应修改,以便您可以直接从表示层使用此方法以及指定的参数。
结论
在此阶段,解决方案和上面的代码生成器并非旨在解决应用程序开发者的所有问题。从我的角度来看,它们的任务可以描述如下:
- 它们有助于在几分钟内创建一个应用程序骨架。此外,在数据结构发生任何变化后,它允许轻松地使所有应用程序层保持最新。
- 解决方案框架定义了实现业务规则、数据挖掘和GUI的特定点。因此,解决方案将不会包含这些项目的混乱。
- 解决方案和代码生成器是协同构建的。
- 有一个元级别(XML声明),我们可以在其中管理生成的代码输出。换句话说,这可以称为“元开发”。
- 它将开发的应用程序划分为严格的层。它允许轻松地管理数据库事务。它为实现业务规则定义了一个严格的位置,这样您的代码就不会混乱。
- 所有活动都在设计时完成,而不是在运行时完成。
- 它们使应用程序开发更易于管理、简单且有趣。
- 它们使应用程序开发者摆脱了常见错误。
- NetTierGenerator应用程序本身可以根据任何特定需求进行调整。