在上一章節中,我們處理了MVC多級目錄問題,參見《二、處理MVC多級目錄問題——以ABP為基礎架構的一個中等規模的OA開發日志》。從這章開始,我們將進入正式的開發過程。首先,我們要完成系統的基礎設置模塊(在后續的功能中,需要大量使用這些基礎設置信息)。和一般的OA系統不同,在律所OA系統中,用戶類別管理是基礎模塊中非常重要、使用頻率非常高的一個基礎模塊。雖然此功能只是很小的一個字典項設置,但是其中涉及了鎖、並發處理、領域服務於應用服務的划分等繁瑣問題。
UI功能頁面介紹(因用戶功能未完成,欠缺刪除頁面)




UI方面,我們使用了Metronic+EasyUI做為主要呈現方式。其中我們對EasyUI做了相應調整,已使其更加適用於Metronic風格。其中,上圖所示的cnblogs.scss為本博客的UI風格(仿小米風格,未完工);color.scss為全局顏色設置;common.scss為全局公用css;easyui.custom.scss為我們對EasyUI的樣式修改;metronic.scss為我們對Metronic的樣式調整; site.scss為程序主css文件。我們在common.scss文件中,導入了color.scss。在site.scss文件中,導入了common.scss、metronic.scss和easyui.custom.scss文件。這樣在布局頁引入css文件時,我們僅需要引入單個site.min.css文件即可,而不用引入一大堆css文件。我們在布局頁中,大量使用了存儲於cdn上的一些css和js文件。這些大多都是一些公用類庫,大家可根據需要自行下載。
ABP對CSRF/XSRF跨站攻擊的處理
abp通過token驗證以解決上述攻擊問題。只要在模板文件(布局頁)中,增加@{SetAntiForgeryCookie();},即可方便在ABP內置的ajax輔助方法中發送生成的token。但在實際使用中,我們需要做一些處理。ABP的view頁面文件繼承自View目錄下EasyFastWebViewPageBase類(該類最終繼承自AbpWebViewPage類),而SetAntiForgeryCookie()方法則是AbpWebViewPage類的內置方法。所以,要使用SetAntiForgeryCookie()方法。我們必須要所有的布局頁全部繼承自EasyFastWebViewPageBase類。


如圖所示,在View根目錄下,有EasyFastWebViewPageBase.cs文件(依據您的項目名,此處會有不同的文件名)。為了保證在Areas中的布局頁也能正常的使用SetAntiForgeryCookie()方法。需要在布局頁繼承該類。(也可以復制一遍,放到所有Areas的View目錄下,但顯然,繼承的方式更合理)感謝ABP架構交流群的朋友們,剛開始時作者也被這里卡了很久,是群里的朋友們最終指出了問題所在。
實體設置

我們在Core層下創建Entities目錄(目錄名可以隨意起,這里采用的常用習慣Entity的復數形式)。然后創建BaseEntity和UserType兩個類。
namespace EasyFast.Core.Entities { public class BaseEntity : FullAuditedEntity<long> { public BaseEntity() { OrderId = 999; Guid = Guid.NewGuid(); } /// <summary> /// model的Guid,用於記錄操作日志 /// </summary> public Guid Guid { get; set; } /// <summary> /// 排序Id /// </summary> [Range(1,999)] public int OrderId { get; set; } /// <summary> /// 行號,用於樂觀並發控制 /// </summary> [Timestamp] public byte[] RowVersion { get; set; } } }
namespace EasyFast.Core.Entities { public class UserType : BaseEntity { /// <summary> /// 人員類別名稱 /// </summary> [StringLength(50)] public string Name { get; set; } /// <summary> /// 備注信息 /// </summary> public string Remarks { get; set; } public ICollection<User> User { get; set; } } }
BaseEntity類將做為我們大多數實體的基類。該基本繼承自FullAuditedEntity<long>。這樣一來,BaseEntity就自動繼承了 public long Id{ get; set; }這個屬性。我們追加的Guid字段用於記錄操作日志。這個在日后使用時再詳細說明。RowVersion字段用於對EF進行並發控制。在SQLServer中,行中的數據每變動一次,RowVersion自動+1(該字段為16進制)。通過對比該字段的變化,我們即可得知在修改或是刪除數據時,是否存在並發沖突。
應用層(EasyFast.Application)主要代碼簡析
特別提醒:本人對應用層、領域層的講解僅僅只是本人的一點淺見,不代表DDD的最佳實踐要求這么干。在本系列文章里,我們更關注解決工程問題,而不是進行理論研究。如您發現我們的設計有不合理之處,或是對ABP的使用或理解有不對之處。歡迎批評指正。
Application層一般稱之為應用服務於層。在DDD設計規范里,此層專門針對頁面進行服務。這個說法可能讓人費解。我們舉個實際的例子做參考:在OA系統中,我們要展示一個律師的信息時,既要展示User表本身的信息,也要同時展示其關聯的Case表、Client表、Finance表等內容。在N層架構的做法中,我們會分別實例化User、Case、Client等對應的業務邏輯類。然后將其查詢的結果存儲成ViewBag發送到View頁面。接着再在View頁面中,將對應的ViewBag轉換成model進行輸出。
public ActionResult Index()
{
long UserID = User.GetUserInfo().LawyerId;
var user = LawyerService.Find(UserID);
if (user.Status == JingShOnline.Models.Enum.LawyerStatus.Normal)
{
return RedirectToAction("Normal", "Authen");
}
ViewBag.CaseList = CaseService.Where(o =>o.LawyerId== UserID)
.Include(o=>o.CaseReason)
.Include(o=>o.Practice)
.Include(o=>o.Court)
.Include(o=>o.Industry)
.Take(10).ToList();
var history = user.LawyerWorkHistory.OrderByDescending(o => o.StartDate).ToList();
var education = user.LawyerEducation.OrderByDescending(o => o.StartDate).ToList();
var academic = user.LawyerAcademic.OrderByDescending(o => o.Id).ToList();
var certificate = user.LawyerCertificate.OrderByDescending(o => o.Id).ToList();
var socialposition = user.LawyerSocialPosition.OrderByDescending(o => o.Id).ToList();
//簡歷完整度
var ResumeCompletion = (history.Count > 0 ? 35 : 0) + (education.Count > 0 ? 35 : 0) + (academic.Count > 0 ? 10 : 0) + (certificate.Count > 0 ? 10 : 0) + (socialposition.Count > 0 ? 10 : 0);
ViewBag.WorkHistory = history;
ViewBag.Education = education;
ViewBag.Academic = academic;
ViewBag.Certificate = certificate;
ViewBag.SocialPosition = socialposition;
ViewBag.ResumeCompletion = ResumeCompletion;
return View(user);
}
參見上述代碼。View頁面需要多個模塊的數據做集中展示。為達到目的,只好在Controller里初始化多個Service,進行多次查詢,然后將查詢的結果存儲到ViewBag中,發送到前台。再在前台進行數據類型轉換並輸出。如此做法主要有兩個個弊端:
- Controller過於重型化,不利於代碼質量控制。在MVC中,Controller應只負責基礎效驗和Action的跳轉。
- ViewBag是弱類型的,前台使用時,容易出錯。且日后代碼進行擴展或是重構時,將大大增大bug出現幾率。
Application層的出現,其實就是為了解決這兩個問題。在DDD設計規范中,Application針對View進行服務,View需要什么類型的數據,那么Application就返回什么類型的數據。在本例子中,Application會返回一個包含了上述所有ViewBag類型的綜合model。這樣就可以把大量代碼轉移到Application或是Core層去實現,且前台只接受一個含有具體數據的model,model是強類型的,不用考慮數據類型轉換問題。

在ABP里,作者推薦為每一個應用服務單獨建立目錄,且每一個應用服務目錄中都應包含Dto子目錄。該目錄用於存放ViewModel。Application層負責將從Core或是Repository中得到的Entity轉化成ViewModel,發送到前台。參見上圖,我們將UserType這個應用服務單獨創建目錄(單獨將UserType視為一個應用服務其實不太合理,更合理的做法是將和User相關的所有內容統一在User這個應用服務中實現)。Dto目錄里存放着UserTypeAppService中每一個方法所對應的輸入輸出參數。ABP推薦將Application中的服務方法所需的參數全部類型化。並分別以Input、Output結尾以做區分。比如我們的刪除用戶類別方法,需要兩個參數:long oldId, long newId,我們仍舊把這兩個參數組成一個類去傳遞。這樣做的好處是,日后進行重構或是功能調整時,會大幅減少程序中的修改地方。降低出現bug的幾率。
namespace EasyFast.Application.UserType
{
public interface IUserTypeAppService : IApplicationService
{
UserTypeInput Find(long id);
long Add(UserTypeInput model);
long Update(UserTypeInput model);
EasyUIDataGrid<UserTypeDataGridDto> GetDataGrid(UserTypeSearch search);
/// <summary>
/// 檢測傳入的全部用戶類別是否含有用戶,用於判斷直接刪除or轉移用戶后再刪除
/// </summary>
/// <param name="ids">long[] Model.UserType.Id</param>
/// <returns>true:含有用戶 false:不含用戶</returns>
bool CheckIsHaveUser(long[] ids);
void Delete(DeleteInput model);
}
}
namespace EasyFast.Application.UserType { public class UserTypeAppService : EasyFastAppServiceBase, IUserTypeAppService { private readonly IRepository<Core.Entities.UserType, long> _userTypeRepository; private readonly IUserTypeService _userTypeService; public UserTypeAppService(IRepository<Core.Entities.UserType, long> userTypeRepository, IUserTypeService userTypeService) { _userTypeRepository = userTypeRepository; _userTypeService = userTypeService; } public UserTypeInput Find(long id) { var data = _userTypeRepository.FirstOrDefault(id); return Mapper.Map<UserTypeInput>(data); } public long Add(UserTypeInput model) { var data = Mapper.Map<Core.Entities.UserType>(model); return _userTypeService.Add(data); } public long Update(UserTypeInput model) { var data = Mapper.Map<Core.Entities.UserType>(model); return _userTypeService.Update(data); } public EasyUIDataGrid<UserTypeDataGridDto> GetDataGrid(UserTypeSearch search) { var data = _userTypeRepository.GetAll() .Where(o => o.Name.Contains(search.Name), !string.IsNullOrEmpty(search.Name)); var total = data.Count(); var list = Mapper.Map<List<UserTypeDataGridDto>>(data); var rows = list.OrderBy(String.Format("{0} {1}", search.Sort, search.Order)) .Skip((search.Page - 1) * search.Rows).Take(search.Rows).ToList(); return new EasyUIDataGrid<UserTypeDataGridDto> { total = total, rows = rows }; } public bool CheckIsHaveUser(long[] ids) { return _userTypeRepository.GetAllIncluding(o => o.User).Where(o => ids.Contains(o.Id)).Any(o => o.User != null); } public void Delete(DeleteInput model) { _userTypeService.Delete(model.OldId, model.NewId); } } }
ABP要求給所有應用服務提取接口,並且接口要繼承自IApplicationService。只有繼承了這個接口,ABP才會自動實現依賴注入。在UserTypeAppService類中,我們自動注入了UserTypeService這個領域服務和UserTypeRepository這個倉儲。除了使用構造參數的注入方式外,您也可以使用屬性注入,但構造參數注入顯得更高大上一點。在作者理解,簡單功能,應用服務直接調用倉儲接口實現。復雜功能(尤指業務邏輯代碼)在領域服務中實現(Core中的Service),然后應用服務調用領域服務的處理結果,返回給用戶。其中,部分功能通過系統默認的倉儲接口無法實現的,就自定義倉儲然后根據情況,選擇應用服務或是領域服務調用並返回。

在我們的設計實現中,新增或是修改人員類別時,要保證不重名,我們沒有采用數據庫唯一性約束,而是通過代碼實現的,先重名檢測,再進行增、改這部分代碼屬於業務邏輯。所以我們將這些代碼放在了領域服務層去實現,應用服務本身不處理這些,其通過調用領域服務中對應的方法並返回合適的結果,供前台使用。
因ViewModel(或者稱為Dto,一個意思,兩種常見名稱)只在Web和Application中使用,所以將AutoMapper相關的映射代碼防止在Application層中最合適不過。我們可以新建一個AutoMapperConfig類,並在其中配置好映射關系后,直接在EasyFastApplicationModule.cs文件中調用即可。不用再web項目中的Global.asax中再次調用,ABP會自動在應用程序初始化時加載我們的配置文件。

namespace EasyFast.Application.AutoMapper
{
public static class AutoMapperConfig
{
public static void Bind(IMapperConfigurationExpression opt)
{
#region UserType
opt.CreateMap<Core.Entities.UserType, UserTypeInput>();
opt.CreateMap<UserTypeInput, Core.Entities.UserType>()
.ForMember(d => d.User, s => s.Ignore());
opt.CreateMap<Core.Entities.UserType, UserTypeDataGridDto>()
.ForMember(d => d.UserCount, s => s.MapFrom(o => o.User.Count));
#endregion
//Mapper.AssertConfigurationIsValid();//驗證所有的映射配置是否都正常
}
public static void Config()
{
Mapper.Initialize(Bind);
}
}
}
namespace EasyFast.Application
{
[DependsOn(typeof(EasyFastCoreModule), typeof(AbpAutoMapperModule))]
public class EasyFastApplicationModule : AbpModule
{
public override void PreInitialize()
{
Configuration.Modules.AbpAutoMapper().Configurators.Add(mapper =>
{
AutoMapperConfig.Bind(mapper);
//Add your custom AutoMapper mappings here...
//mapper.CreateMap<,>()
});
}
public override void Initialize()
{
IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly());
}
}
}
章節過長,未完待續。另征求意見:如此敘述,是否過於繁瑣?如大家普遍認為啰嗦,后續我將省略大部分代碼解釋及配圖說明。只保留關鍵代碼說明及設計思路說明。
