我們在做某件事情的時候,一般需要詳細了解它的特點,以及內在的邏輯關系,一旦我們詳細了解了整個事物后,就可以通過一些輔助手段來提高我們的做事情的效率了。本篇隨筆介紹ABP VNext框架各分層項目的規則,以及結合代碼生成工具Database2Sharp來實現項目類代碼,項目文件等內容的快速生成。
ABP VNext框架在官方下載項目的時候,會生成一個標准的空白項目框架,本代碼工具不是替代這個項目代碼生成,而是基於這個基礎上進行基於數據表的增量式開發模塊的需求(畢竟官方沒有針對數據表的項目代碼生成),最終所有的子模塊可以集成在主模塊上,形成一個完整的系統。
1、ABP VNext框架的項目關系
目前框架代碼生成包括:應用服務層:Application.Contracts和Application項目,領域層:Domain.Shared和Domain項目,基礎設施層:EntityFrameworkCore項目,HTTP 層:HttpApi和HttpApi.Client項目。生成代碼集成相關的基類代碼,簡化項目文件的類代碼。
應用服務層:
Application.Contracts,包含應用服務接口和相關的數據傳輸對象(DTO)。
Application,包含應用服務實現,依賴於 Domain 包和 Application.Contracts 包。
領域層:
Domain.Shared,包含常量,枚舉和其他類型.
Domain 包含實體, 倉儲接口,領域服務接口及其實現和其他領域對象,依賴於 Domain.Shared 包.
基礎設施層:
EntityFrameworkCore,包含EF的ORM處理,使用倉儲模式,實現數據的存儲功能。
HTTP 層
HttpApi項目, 為模塊開發REST風格的HTTP API。
HttpApi.Client項目,它將應用服務接口實現遠程端點的客戶端調用,提供的動態代理HTTP C#客戶端的功能。
各個層的依賴關系如下圖所示。

2、ABP VNext框架各層的項目代碼
我在上篇隨筆《在ABP VNext框架中對HttpApi模塊的控制器進行基類封裝》中介紹了為了簡化子類一些繁復代碼的重復出現,使用自定義基類方式,封裝了一些常用的函數,通過泛型參數的方式,可以完美的實現強類型接口的各種處理。
對於ABP VNext個項目的內容,我們繼續推演到它的項目組織上來。為了簡便,我們以簡單的客戶表T_Customer表來介紹框架項目的分層和關系。

對於這個額外添加的表,首先我們來看看應用服務層的Application.Contracts項目文件,如下所示。

其中映射DTO放在DTO目錄中,而應用服務的接口定義則放在Interface目錄中,使用目錄的好處是利於查看和管理,特別是在業務表比較多的情況下。
DTO類的定義如下所示。

其中用到了基類對象EntityDto、CreationAuditedEntityDto、AuditEntityDto、FullAuditedEntityDto幾個基類DTO對象,具體采用哪個基類DTO,依賴於我們表的包含哪些系統字段。如只包含CreationTime、CreatorId那么就采用CreationAuditedEntityDto,其他的依次類推。
領域層的實體類關系和前面DTO關系類似,如下所示。

這樣我們利用代碼生成工具生成代碼的時候,就需要判斷表的系統字段有哪些來使用不同的系統DTO基類了。
而應用服務層的接口定義文件如下所示,它使用了我們前面隨筆介紹過的自定義基類或接口。

通過傳入泛型類型,我們可以構建強類型化的接口定義。
應用服務層的Application項目包含DTO映射文件和應用服務層接口實現類,如下所示。

其中映射DTO、Domain Entity(領域實體)關系的Automapper文件放在MapProfile文件夾中,而接口實現類文件則放在Service目錄中,也是方便管理。
映射類文件,主要定義DTO和Domain Entity(領域實體)關系,如下所示。

這樣文件單獨定義,在模塊中會統一加載整個程序集的映射文件,比較方便。
public class TestProjectApplicationModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { Configure<AbpAutoMapperOptions>(options => { options.AddMaps<TestProjectApplicationModule>(); }); } }
應用服務層的接口實現如下定義所示。
/// <summary> /// Customer,應用層服務接口實現 /// </summary> public class CustomerAppService : MyCrudAppService<Customer, CustomerDto, string, CustomerPagedDto, CreateCustomerDto, CustomerDto>, ICustomerAppService
通過繼承相關的自定義基類,可以統一封裝一些常見的接口實現,傳入對應的泛型類型,可以構建強類型的接口實現。
另外實現類還需要包含一些方法的重載,以重寫某些規則,如排序、查詢處理、以及一些通用的信息轉義等,詳細的應用服務層接口實現代碼如下所示。
/// <summary> /// Customer,應用層服務接口實現 /// </summary> public class CustomerAppService : MyCrudAppService<Customer, CustomerDto, string, CustomerPagedDto, CreateCustomerDto, CustomerDto>, ICustomerAppService { private readonly IRepository<Customer, string> _repository;//業務對象倉儲對象 public CustomerAppService(IRepository<Customer, string> repository) : base(repository) { _repository = repository; } /// <summary> /// 自定義條件處理 /// </summary> /// <param name="input">查詢條件Dto</param> /// <returns></returns> protected override async Task<IQueryable<Customer>> CreateFilteredQueryAsync(CustomerPagedDto input) { var query = await base.CreateFilteredQueryAsync(input); query = query .WhereIf(!input.ExcludeId.IsNullOrWhiteSpace(), t=>t.Id != input.ExcludeId) //不包含排除ID .WhereIf(!input.Name.IsNullOrWhiteSpace(), t => t.Name.Contains(input.Name)) //如需要精確匹配則用Equals //區間查詢 .WhereIf(input.AgeStart.HasValue, s => s.Age >= input.AgeStart.Value) .WhereIf(input.AgeEnd.HasValue, s => s.Age <= input.AgeEnd.Value) //創建日期區間查詢 .WhereIf(input.CreationTimeStart.HasValue, s => s.CreationTime >= input.CreationTimeStart.Value) .WhereIf(input.CreationTimeEnd.HasValue, s => s.CreationTime <= input.CreationTimeEnd.Value) ; return query; } /// <summary> /// 自定義排序處理 /// </summary> /// <param name="query">可查詢LINQ</param> /// <param name="input">查詢條件Dto</param> /// <returns></returns> protected override IQueryable<Customer> ApplySorting(IQueryable<Customer> query, CustomerPagedDto input) { //按創建時間倒序排序 return base.ApplySorting(query, input).OrderByDescending(s => s.CreationTime);//時間降序 //先按第一個字段排序,然后再按第二字段排序 //return base.ApplySorting(query, input).OrderBy(s=>s.DictType_ID).ThenBy(s => s.Seq); } /// <summary> /// 獲取字段中文別名(用於界面顯示)的字典集合 /// </summary> /// <returns></returns> public override Task<Dictionary<string, string>> GetColumnNameAlias() { Dictionary<string, string> dict = new Dictionary<string, string>(); #region 添加別名解析 //系統部分字段 dict.Add("Id", "編號"); dict.Add("UserName", "用戶名"); dict.Add("Creator", "創建人"); dict.Add("CreatorUserName", "創建人"); dict.Add("CreationTime", "創建時間"); //其他字段 dict.Add("Name", "姓名"); dict.Add("Age", ""); #endregion return Task.FromResult(dict); } /// <summary> /// 對記錄進行轉義 /// </summary> /// <param name="item">dto數據對象</param> /// <returns></returns> protected override void ConvertDto(CustomerDto item) { //如需要轉義,則進行重寫 #region 參考代碼 //用戶名稱轉義 //if (item.Creator.HasValue) //{ // //需在CustomerDto中增加CreatorUserName屬性 // var user = _userRepository.FirstOrDefault(item.Creator.Value); // if (user != null) // { // item.CreatorUserName = user.UserName; // } //} //if (item.UserId.HasValue) //{ // item.UserName = _userRepository.Get(item.UserId.Value).UserName; //} //IP地址轉義 //if (!string.IsNullOrEmpty(item.ClientIpAddress)) //{ // item.ClientIpAddress = item.ClientIpAddress.Replace("::1", "127.0.0.1"); //} #endregion } /// <summary> /// 用於測試的額外接口 /// </summary> public Task<bool> TestExtra() { return Task.FromResult(true); } }
這些與T_Customer 表相關的信息,如表信息,字段信息等相關的內容,可以通過代碼生成工具元數據進行統一處理即可。
領域層的內容,包含Domain、Domain.Share兩個項目,內容和Applicaiton.Contracts項目類似,主要定義一些實體相關的內容,這部分也是根據表和表的字段進行按規則生成。而其中一些類則根據命名控件和項目名稱構建即可。

而Domain項目中的Customer領域實體定義代碼如下所示。

而領域實體和聚合根的基類關系如下所示。

具體使用***Entity(如FullAuditedEntity基類)還是使用聚合根***AggregateRoot(如FullAuditedAggregateRoot)作為領域實體的基類,生成的時候,我們需要判斷表的字段關系即可,如果表包含ExtraProperties和ConcurrencyStamp,則使用聚合根相關的基類。

我們的T_Customer包含聚合根基類所需要的字段,代碼生成的時候,則基類應該使用FullAuditedAggregateRoot<T>基類。
對於EntityFrameworkCore項目文件,它主要就是生成對應表的DbSet然后用於操作即可。

其中DbContext文件如下所示
namespace WHC.TestProject.EntityFrameworkCore { [ConnectionStringName("Default")] public class TestProjectDbContext : AbpDbContext<TestProjectDbContext> { /// <summary> /// T_Customer,數據表對象 /// </summary> public virtual DbSet<Customer> Customers { get; set; } public TestProjectDbContext(DbContextOptions<TestProjectDbContext> options) : base(options) { } protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.ConfigureTestProject(); } } }
按照規則生成即可,其他的可以不管了。
我們注意一下EntityFrameworkCoreModule中的內容處理,如果不是采用聚合根作為領域實體的基類,而是采用**Entity標准實體(如FullAuditedEntity基類)作為基類,那么需要在該文件中默認設置為true處理,因為ABP VNext框架默認只是加入聚合根的領域實體處理。
namespace WHC.TestProject.EntityFrameworkCore { [DependsOn( typeof(TestProjectDomainModule), typeof(AbpEntityFrameworkCoreModule) )] public class TestProjectEntityFrameworkCoreModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddAbpDbContext<TestProjectDbContext>(options => { //options.AddDefaultRepositories(); //默認情況下,這將為每個聚合根實體(從派生的類AggregateRoot)創建一個存儲庫。 //如果您也想為其他實體創建存儲庫,請設置includeAllEntities為true: //參考https://docs.abp.io/en/abp/latest/Entity-Framework-Core#add-default-repositories options.AddDefaultRepositories(includeAllEntities: true); }); } } }
上面的處理,即使是采用**Entity標准實體(如FullAuditedEntity基類)作為基類,也沒問題了,可以順利反射Repository對象出來了。
而對於Http項目分層,包含的HttpApi項目和HttpApi.Client,前者是重從應用服務層的服務接口,使用知道自定義的API規則,雖然默認可以使用應用服務層ApplicationService的自動API發布,不過為了更強的控制規則,建議重寫(也是官方的做法)。兩個項目文件如下所示。

其中HttpApi項目控制器,也是采用了此前介紹過的自定義基類,可以減少重復代碼的處理。
namespace WHC.TestProject.Controllers { /// <summary> /// Customer,控制器對象 /// </summary> //[RemoteService] //[Area("crm")] [ControllerName("Customer")] [Route("api/Customer")] public class CustomerController : MyAbpControllerBase<CustomerDto, string, CustomerPagedDto,CreateCustomerDto, CustomerDto>, ICustomerAppService { private readonly ICustomerAppService _appService; public CustomerController(ICustomerAppService appService) : base(appService) { _appService = appService; } } }
其中MyAbpControllerBase控制器基類,封裝了很多常見的CRUD方法(Create/Update/Delete/GetList/Get),以及一些BatchDelete、Count、GetColumnNameAlias等基礎方法。

對於ABP VNext各個項目的項目文件的生成,我們這里順便說說,其實這個文件很簡單,沒有太多的內容,包含命名空間,項目名稱,以及一些常見的引用而已,它本身也是一個XML文件,填入相關信息生成文件即可。

而對於解決方案,它就是包含不同的項目文件,以及各個項目文件有一個獨立的GUID,因此我們動態構建對應的GUID值,然后綁定在模板上即可。

代碼工具中,后端提供數據綁定到前端模板即可實現內容的動態化了。

3、使用代碼生成工具Database2Sharp生成ABP VNext框架項目
上面介紹了ABP VNext框架的各個項目層的代碼生成,以及一些代碼生成處理的規則,那么實際的代碼生成工具生成是如何的呢?
代碼生成工具下載地址:http://www.iqidi.com/database2sharp.htm。
首先單擊左側節點展開ABP VNext項目的數據庫,讓數據庫的元數據讀取出來,便於后面的代碼生成。

然后從右鍵菜單中選擇【代碼生成】【ABP VNext框架代碼生成】或者工具欄中選擇快速入口,一樣的效果。

在彈出的對話框中選擇相關的數據表,用於生成框架代碼即可,注意修改合適的主命名空間,可以是TestProject或者WHC.Project等類似的名稱。

最后下一步生成確認即可生成相關的解決方案代碼。

生成后所有項目關系已經完善,可以直接打開解決方案查看到整個項目情況如下所示。

這樣生成的解決方案,可以編譯為一單獨的模塊,需要的時候,直接在主項目中引用並添加依賴即可。
例如我們在一個ABP VNext的標准項目MicroBookStore.Web中引入剛才代碼生成工具生成的模塊,那么在MicroBookStoreWebModule.cs 中添加依賴即可,如下所示。

由於我們是采用DLL的引用方式,那么在項目添加對應的引用關系才可以使用。

同時在EFCore項目中添加項目的TestProject項目的EF依賴關系如下所示。

這樣跑動起來項目,就可以有Swagger的接口可以查看並統一調用了。

以上就是對於ABP VNext框架項目的分析和項目關系的介紹,並重要介紹利用代碼生成工具來輔助增量式模塊化開發的操作處理,這樣我們在開發ABP VNext項目的時候,更加方便高效了。
