在前面隨筆《ABP開發(fā)框架前后端開發(fā)系列---(1)框架的總體介紹》大概介紹了這個ABP框架的主要特點,以及介紹了我對這框架的Web API應用優(yōu)先的一些看法,本篇繼續(xù)探討ABP框架的初步使用,也就是我們下載到的ABP框架項目(基于ABP基礎項目的擴展項目),如果理解各個組件模塊,以及如何使用。
1)ABP框架應用項目的介紹
整個基礎的ABP框架看似非常龐大,其實很多項目也很少內(nèi)容,主要是獨立封裝不同的組件進行使用,如Automaper、SignalR、MongoDB、Quartz。。。等等內(nèi)容,基本上我們主要關注的內(nèi)容就是Abp這個主要的項目里面,其他的是針對不同的組件應用做的封裝。
而基于基礎ABP框架擴展出來的ABP應用項目,則簡單很多,我們也是在需要用到不同組件的時候,才考慮引入對應的基礎模塊進行使用,一般來說,主要還是基于倉儲管理實現(xiàn)基于數(shù)據(jù)庫的應用,因此我們主要對微軟的實體框架的相關內(nèi)容了解清楚即可。
這個項目是一個除了包含基礎的人員、角色、權(quán)限、認證、配置信息的基礎項目外,而如果你從這里開始,對于其中的一些繼承關系的了解,會增加很多困難,因為它們基礎的用戶、角色等對象關系實在是很復雜。
我建議從一個簡單的項目開始,也就是基于一兩個特定的應用表開始的項目,因此可以參考案例項目:eventcloud 或者 sample-blog-module 項目,我們?nèi)腴T理解起來可能更加清楚。這里我以eventcloud項目來進行分析項目中各個層的類之間的關系。
我們先從一個關系圖來了解下框架下的領域驅(qū)動模塊中的各個類之間的關系。
先以領域?qū)樱簿褪琼椖恐械腅ventCloud.Core里面的內(nèi)容進行分析。
2)領域?qū)ο髮拥拇a分析
首先,我們需要了解領域?qū)ο蠛蛿?shù)據(jù)庫之間的關系的類,也就是領域?qū)嶓w信息,這個類非常關鍵,它是構(gòu)建倉儲模式和數(shù)據(jù)庫表之間的關系的。
[Table("AppEvents")]
public class Event : FullAuditedEntity<Guid>, IMustHaveTenant
{
public virtual int TenantId { get; set; }
[Required]
[StringLength(MaxTitleLength)]
public virtual string Title { get; protected set; }
[StringLength(MaxDescriptionLength)]
public virtual string Description { get; protected set; }
public virtual DateTime Date { get; protected set; }
public virtual bool IsCancelled { get; protected set; }
......
}
這個里面定義了領域?qū)嶓w和表名之間的關系,其他屬性也就是對應數(shù)據(jù)庫的字段了
[Table("AppEvents")]
然后在EventCloud.EntityFrameworkCore項目里面,加入這個表的DbSet對象,如下代碼所示。
namespace EventCloud.EntityFrameworkCore
{
public class EventCloudDbContext : AbpZeroDbContext<Tenant, Role, User, EventCloudDbContext>
{
public virtual DbSet<Event> Events { get; set; }
public virtual DbSet<EventRegistration> EventRegistrations { get; set; }
public EventCloudDbContext(DbContextOptions<EventCloudDbContext> options)
: base(options)
{
}
}
}
簡單的話,倉儲模式就可以跑起來了,我們利用 IRepository<Event, Guid> 接口就可以獲取對應表的很多處理接口,包括增刪改查、分頁等等接口,不過為了進行業(yè)務邏輯的隔離,我們引入了Application Service應用層,同時也引入了DTO(數(shù)據(jù)傳輸對象)的概念,以便向應用層隱藏我們的領域?qū)ο笮畔ⅲ瑢崿F(xiàn)更加彈性化的處理。一般和領域?qū)ο髮腄TO對象定義如下所示。
[AutoMapFrom(typeof(Event))]
public class EventListDto : FullAuditedEntityDto<Guid>
{
public string Title { get; set; }
public string Description { get; set; }
public DateTime Date { get; set; }
public bool IsCancelled { get; set; }
public virtual int MaxRegistrationCount { get; protected set; }
public int RegistrationsCount { get; set; }
}
其中我們需要注意實體類繼承自FullAuditedEntityDto<Guid>,它標記這個領域?qū)ο髸涗泟?chuàng)建、修改、刪除的標記、時間和人員信息,如果需要深入了解這個部分,可以參考下ABP官網(wǎng)關于領域?qū)嶓w對象的介紹內(nèi)容(Entities)。
通過在類增加標記性的特性處理,我們可以從Event領域?qū)ο蟮紼ventListDto的對象實現(xiàn)了自動化的映射。這樣的定義處理,一般來說沒有什么問題,但是如果我們需要把DTO(如EventListDto)隔離和領域?qū)ο螅ㄈ鏓vent)的關系,把DTO單獨抽取來方便公用,那么我們可以在應用服務層定義一個領域?qū)ο蟮挠成湮募硖娲@種聲明式的映射關系,AutoMaper的映射文件定義如下所示。
public class EventMapProfile : Profile
{
public EventMapProfile()
{
CreateMap<EventListDto, Event>();
CreateMap<EventDetailOutput, Event>();
CreateMap<EventRegistrationDto, EventRegistration>();
}
}
這樣抽取獨立的映射文件,可以為我們單獨抽取DTO對象和應用層接口作為一個獨立項目提供方便,因為不需要依賴領域?qū)嶓w。如我改造項目的DTO層實例如下所示。
剛才介紹了領域?qū)嶓w和DTO對象的映射關系,就是為了給應用服務層提供數(shù)據(jù)的承載。
如果領域?qū)ο蟮倪壿嬏幚肀容^復雜一些,還可以定義一個類似業(yè)務邏輯類(類似我們說說的BLL),一般ABP框架里面以Manager結(jié)尾的就是這個概念,如對于案例里面,業(yè)務邏輯接口和邏輯類定義如下所示,這里注意接口繼承自IDomainService接口。
/// <summary>
/// Event的業(yè)務邏輯類
/// </summary>
public interface IEventManager: IDomainService
{
Task<Event> GetAsync(Guid id);
Task CreateAsync(Event @event);
void Cancel(Event @event);
Task<EventRegistration> RegisterAsync(Event @event, User user);
Task CancelRegistrationAsync(Event @event, User user);
Task<IReadOnlyList<User>> GetRegisteredUsersAsync(Event @event);
}
業(yè)務邏輯類的實現(xiàn)如下所示。
我們看到這個類的構(gòu)造函數(shù)里面,帶入了幾個接口對象的參數(shù),這個就是DI,依賴注入的概念,這些通過IOC容易進行構(gòu)造函數(shù)的注入,我們只需要知道,在模塊啟動后,這些接口都可以使用就可以了,如果需要了解更深入的,可以參考ABP官網(wǎng)對于依賴注入的內(nèi)容介紹(Dependency Injection)。
這樣我們對應的Application Service里面,對于Event的應用服務層的類EventAppService ,如下所示。
[AbpAuthorize]
public class EventAppService : EventCloudAppServiceBase, IEventAppService
{
private readonly IEventManager _eventManager;
private readonly IRepository<Event, Guid> _eventRepository;
public EventAppService(
IEventManager eventManager,
IRepository<Event, Guid> eventRepository)
{
_eventManager = eventManager;
_eventRepository = eventRepository;
}
......
這里的服務層類提供了兩個接口注入,一個是自定義的事件業(yè)務對象類,一個是標準的倉儲對象。
大多數(shù)情況下如果是基于Web API的架構(gòu)下,如果是基于數(shù)據(jù)庫表的處理,我覺得領域的業(yè)務管理類也是不必要的,直接使用倉儲的標準對象處理,已經(jīng)可以滿足大多數(shù)的需要了,一些邏輯我們可以在Application Service里面實現(xiàn)以下即可。
3)字典模塊業(yè)務類的簡化
我們以字典模塊的字典類型表來介紹。
領域業(yè)務對象接口層定義如下所示(類似IBLL)
/// <summary>
/// 領域業(yè)務管理接口
/// </summary>
public interface IDictTypeManager : IDomainService
{
/// <summary>
/// 獲取所有字典類型的列表集合(Key為名稱,Value為ID值)
/// </summary>
/// <param name="dictTypeId">字典類型ID,為空則返回所有</param>
/// <returns></returns>
Task<Dictionary<string, string>> GetAllType(string dictTypeId);
}
領域業(yè)務對象管理類(類似BLL)
/// <summary>
/// 領域業(yè)務管理類實現(xiàn)
/// </summary>
public class DictTypeManager : DomainService, IDictTypeManager
{
private readonly IRepository<DictType, string> _dictTypeRepository;
public DictTypeManager(IRepository<DictType, string> dictTypeRepository)
{
this._dictTypeRepository = dictTypeRepository;
}
/// <summary>
/// 獲取所有字典類型的列表集合(Key為名稱,Value為ID值)
/// </summary>
/// <param name="dictTypeId">字典類型ID,為空則返回所有</param>
/// <returns></returns>
public async Task<Dictionary<string, string>> GetAllType(string dictTypeId)
{
IList<DictType> list = null;
if (!string.IsNullOrWhiteSpace(dictTypeId))
{
list = await _dictTypeRepository.GetAllListAsync(p => p.PID == dictTypeId);
}
else
{
list = await _dictTypeRepository.GetAllListAsync();
}
Dictionary<string, string> dict = new Dictionary<string, string>();
foreach (var info in list)
{
if (!dict.ContainsKey(info.Name))
{
dict.Add(info.Name, info.Id);
}
}
return dict;
}
}
然后領域?qū)ο蟮膽梅諏咏涌趯崿F(xiàn)如下所示
[AbpAuthorize]
public class DictTypeAppService : MyAsyncServiceBase<DictType, DictTypeDto, string, PagedResultRequestDto, CreateDictTypeDto, DictTypeDto>, IDictTypeAppService
{
private readonly IDictTypeManager _manager;
private readonly IRepository<DictType, string> _repository;
public DictTypeAppService(
IRepository<DictType, string> repository,
IDictTypeManager manager) : base(repository)
{
_repository = repository;
_manager = manager;
}
/// <summary>
/// 獲取所有字典類型的列表集合(Key為名稱,Value為ID值)
/// </summary>
/// <returns></returns>
public async Task<Dictionary<string, string>> GetAllType(string dictTypeId)
{
var result = await _manager.GetAllType(dictTypeId);
return result;
}
......
這樣就在應用服務層里面,就整合了業(yè)務邏輯類的處理,不過這樣的做法,對于常規(guī)數(shù)據(jù)庫的處理來說,顯得有點累贅,還需要多定義一個業(yè)務對象接口和一個業(yè)務對象實現(xiàn),同時在應用層接口里面,也需要多增加一個接口參數(shù),總體感覺有點多余,因此我把它改為使用標準的倉儲對象來處理就可以達到同樣的目的了。
在項目其中對應位置,刪除字典類型的一個業(yè)務對象接口和一個業(yè)務對象實現(xiàn),改為標準倉儲對象的接口處理,相當于把業(yè)務邏輯里面的代碼提出來放在服務層而已,那么在應用服務層的處理代碼如下所示。
[AbpAuthorize]
public class DictTypeAppService : MyAsyncServiceBase<DictType, DictTypeDto, string, PagedResultRequestDto, CreateDictTypeDto, DictTypeDto>, IDictTypeAppService
{
private readonly IRepository<DictType, string> _repository;
public DictTypeAppService(
IRepository<DictType, string> repository) : base(repository)
{
_repository = repository;
}
/// <summary>
/// 獲取所有字典類型的列表集合(Key為名稱,Value為ID值)
/// </summary>
/// <returns></returns>
public async Task<Dictionary<string, string>> GetAllType(string dictTypeId)
{
IList<DictType> list = null;
if (!string.IsNullOrWhiteSpace(dictTypeId))
{
list = await Repository.GetAllListAsync(p => p.PID == dictTypeId);
}
else
{
list = await Repository.GetAllListAsync();
}
Dictionary<string, string> dict = new Dictionary<string, string>();
foreach (var info in list)
{
if (!dict.ContainsKey(info.Name))
{
dict.Add(info.Name, info.Id);
}
}
return dict;
}
......
這樣我們少定義兩個文件,以及減少協(xié)調(diào)業(yè)務類的代碼,代碼更加簡潔和容易理解,反正最終實現(xiàn)都是基于倉儲對象的接口調(diào)用。
另外,我們繼續(xù)了解項目,知道在Web.Host項目是我們Web API層啟動,且動態(tài)構(gòu)建Web API層的服務層。它整合了Swagger對接口的測試使用。
// Swagger - Enable this line and the related lines in Configure method to enable swagger UI
services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new Info { Title = "MyProject API", Version = "v1" });
options.DocInclusionPredicate((docName, description) => true);
// Define the BearerAuth scheme that's in use
options.AddSecurityDefinition("bearerAuth", new ApiKeyScheme()
{
Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"",
Name = "Authorization",
In = "header",
Type = "apiKey"
});
// Assign scope requirements to operations based on AuthorizeAttribute
options.OperationFilter<SecurityRequirementsOperationFilter>();
});
啟動項目,我們可以看到Swagger的管理界面如下所示。