一個業務模塊,是負責完成一系列功能的,這些功能相互之間具有密切的關聯性,所以對於一個模塊來說,業務服務是一個整體,不應把他們再按單個實體拆分開來。OSharp 的業務模塊代碼結構設計,也是根據這一原則來設計的。設計規則如下 ...
什麼是OSharp
OSharpNS全稱OSharp Framework with .NetStandard2.0,是一個基於.NetStandard2.0
開發的一個.NetCore
快速開發框架。這個框架使用最新穩定版的.NetCore SDK
(當前是.NET Core 2.2),對 AspNetCore 的配置、依賴註入、日誌、緩存、實體框架、Mvc(WebApi)、身份認證、許可權授權等模塊進行更高一級的自動化封裝,並規範了一套業務實現的代碼結構與操作流程,使 .Net Core 框架更易於應用到實際項目開發中。
- 開源地址:https://github.com/i66soft/osharp
- 官方示例:https://www.osharp.org
- 文檔中心:https://docs.osharp.org
- VS 插件:https://marketplace.visualstudio.com/items?itemName=LiuliuSoft.osharp
- 系列示例:https://github.com/i66soft/osharp-docs-samples
概述
一個模塊的服務層,主要負責如下幾個方面的工作:
- 向 API層 提供各個實體的數據查詢的
IQueryable<T>
類型的數據源 - 接收 API層 傳入的
IInputDto
參數,完成實體的新增
、更新
、刪除
等業務操作 - 接收 API層 傳入的參數,處理各種
模塊級別
的綜合業務 - 處理完業務之後,將數據通過 數據倉儲
IRepository
更新到資料庫 - 向
事件匯流排
模塊發佈業務處理事件,觸發訂閱的業務事件 - 向 API 層返回業務操作結果
整個過程如下圖所示:
服務層代碼佈局
服務層代碼佈局分析
一個業務模塊,是負責完成一系列功能的,這些功能相互之間具有密切的關聯性,所以對於一個模塊來說,業務服務是一個整體,不應把他們再按單個實體拆分開來。
OSharp 的業務模塊代碼結構設計,也是根據這一原則來設計的。設計規則如下:
- 服務介面
IBlogsContract
:一個模塊的業務服務共用一個服務介面,介面中包含模塊的綜合業務服務,也包含模塊的各個實體的查詢數據集、新增、更新、刪除等自有業務服務。 - 服務實現
BlogsService
:服務實現使用 分部類partial
設計,例如本例中的博客模塊業務,文件拆分如下:BlogsService.cs
:博客模塊服務實現類的主文件,負責各實體的倉儲服務註入,輔助服務註入,模塊綜合業務
實現BlogsService.Blog.cs
:博客模塊服務的博客實體
服務實現類,負責博客實體的查詢數據集、增改刪
業務實現BlogsService.Post.cs
:博客模塊服務的文章實體
服務實現類,負責文章實體的查詢數據集、增改刪
業務實現
- 模塊入口
BlogsPack
:定義模塊的級別、啟動順序、執行服務添加、模塊初始化等功能
綜上,服務層代碼佈局如下所示:
src # 源代碼文件夾
└─Liuliu.Blogs.Core # 項目核心工程
└─Blogs # 博客模塊文件夾
├─Events # 業務事件文件夾
│ ├─VerifyBlogEventData.cs # 審核博客事件數據
│ └─VerifyBlogEventHandler.cs # 審核博客事件處理器
├─BlogsPack.cs # 博客模塊入口類
├─BlogsService.cs # 博客服務類
├─BlogsService.Blog.cs # 博客模塊-博客服務類
├─BlogsService.Post.cs # 博客模塊-文章服務類
└─IBlogsContract.cs # 博客模塊服務介面
服務介面 IBlogsContract
介面定義分析
數據查詢
對於數據查詢,業務層只向 API 層開放一個 IQueryable<TEntity>
的查詢數據集。原則上,服務層不實現 純數據查詢
(例如 用於列表分頁數據、下拉菜單選項 等數據,不涉及數據變更的查詢操作) 的服務,所有的 純數據查詢
都在 API層 按需要進行查詢。具體分析請看 >>數據查詢應該在哪做>>
額外的,根據一定條件判斷一個數據是否存在
這種需求經常會用到(例如在新增或修改一個要求唯一的字元串時,需要非同步檢查輸入的字元串是否已存在),因此設計一個 檢查實體是否存在CheckEntityExists
的服務很有必要。
!!!node
對於新增、更新、刪除操作,除非很確定一次只操作一條記錄除外,為了支持可能的批量操作,設計上都應把服務層的 增改刪 操作設計為數組型參數的批量操作,同時使用 params
關鍵字使操作支持單個數據操作。
數據變更
對於每一個實體,服務層按 業務需求分析
的要求定義必要的 新增、更新、刪除
等操作,OSharp框架定義了一個 業務操作結果信息類 OperationResult 來封裝業務操作結果,這個結果可以返回 操作結果類型(成功/錯誤/未變化/不存在/驗證失敗
)、返回消息、返回附加數據 等豐富的信息,API層 接受操作結果後可進行相應的處理。
博客模塊的介面定義
回到我們的 Liuliu.Blogs
項目,根據 <業務模塊設計#服務層> 的需求分析,我們需要給 博客Blog
實體定義 申請開通、開通審核、更新、刪除
服務,給 文章Post
實體類定義 新增、更新、刪除
服務。
介面定義如下:
/// <summary>
/// 業務契約介面:博客模塊
/// </summary>
public interface IBlogsContract
{
#region 博客信息業務
/// <summary>
/// 獲取 博客信息查詢數據集
/// </summary>
IQueryable<Blog> Blogs { get; }
/// <summary>
/// 檢查博客信息是否存在
/// </summary>
/// <param name="predicate">檢查謂語表達式</param>
/// <param name="id">更新的博客信息編號</param>
/// <returns>博客信息是否存在</returns>
Task<bool> CheckBlogExists(Expression<Func<Blog, bool>> predicate, int id = 0);
/// <summary>
/// 申請博客信息
/// </summary>
/// <param name="dto">申請博客信息DTO信息</param>
/// <returns>業務操作結果</returns>
Task<OperationResult> ApplyForBlog(BlogInputDto dto);
/// <summary>
/// 審核博客信息
/// </summary>
/// <param name="id">博客編號</param>
/// <param name="isEnabled">是否通過</param>
/// <returns>業務操作結果</returns>
Task<OperationResult> VerifyBlog(int id, bool isEnabled);
/// <summary>
/// 更新博客信息
/// </summary>
/// <param name="dtos">包含更新信息的博客信息DTO信息</param>
/// <returns>業務操作結果</returns>
Task<OperationResult> UpdateBlogs(params BlogInputDto[] dtos);
/// <summary>
/// 刪除博客信息
/// </summary>
/// <param name="ids">要刪除的博客信息編號</param>
/// <returns>業務操作結果</returns>
Task<OperationResult> DeleteBlogs(params int[] ids);
#endregion
#region 文章信息業務
/// <summary>
/// 獲取 文章信息查詢數據集
/// </summary>
IQueryable<Post> Posts { get; }
/// <summary>
/// 檢查文章信息是否存在
/// </summary>
/// <param name="predicate">檢查謂語表達式</param>
/// <param name="id">更新的文章信息編號</param>
/// <returns>文章信息是否存在</returns>
Task<bool> CheckPostExists(Expression<Func<Post, bool>> predicate, int id = 0);
/// <summary>
/// 添加文章信息
/// </summary>
/// <param name="dtos">要添加的文章信息DTO信息</param>
/// <returns>業務操作結果</returns>
Task<OperationResult> CreatePosts(params PostInputDto[] dtos);
/// <summary>
/// 更新文章信息
/// </summary>
/// <param name="dtos">包含更新信息的文章信息DTO信息</param>
/// <returns>業務操作結果</returns>
Task<OperationResult> UpdatePosts(params PostInputDto[] dtos);
/// <summary>
/// 刪除文章信息
/// </summary>
/// <param name="ids">要刪除的文章信息編號</param>
/// <returns>業務操作結果</returns>
Task<OperationResult> DeletePosts(params int[] ids);
#endregion
}
服務實現 BlogsService
依賴服務註入方式分析
服務層的業務實現,通過向服務實現類註入 數據倉儲IRepository<TEntity, TKey>
對象來獲得向資料庫存取數據的能力。根據 .NetCore 的依賴註入使用原則,常規的做法是在服務實現類的 構造函數
進行依賴服務的註入。形如:
/// <summary>
/// 業務服務實現:博客模塊
/// </summary>
public class BlogsService : IBlogsContract
{
private readonly IRepository<Blog, int> _blogRepository;
private readonly IRepository<Post, int> _postRepository;
private readonly IRepository<User, int> _userRepository;
private readonly IRepository<Role, int> _roleRepository;
private readonly IRepository<UserRole, Guid> _userRoleRepository;
private readonly IEventBus _eventBus;
/// <summary>
/// 初始化一個<see cref="BlogsService"/>類型的新實例
/// </summary>
public BlogsService(IRepository<Blog, int> blogRepository,
IRepository<Post, int> postRepository,
IRepository<User, int> userRepository,
IRepository<Role, int> roleRepository,
IRepository<UserRole, Guid> userRoleRepository,
IEventBus eventBus)
{
_blogRepository = blogRepository;
_postRepository = postRepository;
_userRepository = userRepository;
_roleRepository = roleRepository;
_userRoleRepository = userRoleRepository;
_eventBus = eventBus;
}
}
構造函數註入帶來的性能影響
每個倉儲都使用構造函數註入的話,如果模塊的業務比較複雜,涉及的實體比較多(比如十幾個實體是很經常的事),就會造成每次實例化 BlogsService
類的實例的時候,都需要去實例化很多個依賴服務,而實際上 一次業務執行只執行服務中的某個方法,可能也就用到其中的一兩個依賴服務,這就造成了很多不必要的額外工作,也就是性能損耗。
依賴服務註入的性能優化
如果 不考慮業務服務的可測試性(單元測試通常需要Mock依賴服務)的話,在構造函數中只註入 IServiceProvider
實例,然後在業務代碼中使用 serviceProvider.GetService<T>()
的方式來 按需獲取 依賴服務的實例,是比較經濟的方式。則服務實現變為如下所示:
/// <summary>
/// 業務服務實現:博客模塊
/// </summary>
public class BlogsService : IBlogsContract
{
private readonly IServiceProvider _serviceProvider;
/// <summary>
/// 初始化一個<see cref="BlogsService"/>類型的新實例
/// </summary>
public BlogsService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
/// <summary>
/// 獲取 博客倉儲對象
/// </summary>
protected IRepository<Blog, int> BlogRepository => _serviceProvider.GetService<IRepository<Blog, int>>();
/// <summary>
/// 獲取 文章倉儲對象
/// </summary>
protected IRepository<Post, int> PostRepository => _serviceProvider.GetService<IRepository<Post, int>>();
/// <summary>
/// 獲取 用戶倉儲對象
/// </summary>
protected IRepository<User, int> UserRepository => _serviceProvider.GetService<IRepository<User, int>>();
/// <summary>
/// 獲取 角色倉儲對象
/// </summary>
protected IRepository<Role, int> RoleRepository => _serviceProvider.GetService<IRepository<Role, int>>();
/// <summary>
/// 獲取 角色倉儲對象
/// </summary>
protected IRepository<UserRole, Guid> UserRoleRepository => _serviceProvider.GetService<IRepository<UserRole, Guid>>();
/// <summary>
/// 獲取 事件匯流排對象
/// </summary>
protected IEventBus EventBus => _serviceProvider.GetService<IEventBus>();
}
各個依賴服務改為屬性的存在方式,並且可訪問性為 protected
,這就保證了依賴服務的安全性。依賴服務使用 serviceProvider.GetService<T>()
的方式創建實例,可以做到 按需創建,達到性能優化的目的。
增改刪操作的簡化
常規批量操作的弊端
直接通過 數據倉儲IRepository<TEntity, TKey>
實現數據的增改刪的批量操作,總免不了要使用迴圈來遍歷傳進來的多個InputDto,例如文章的更新操作,以下幾個步驟是免不了的:
- 由
dto.Id
查找出相應的文章實體entity
,如果不存在,中止操作並返回 - 進行更新前的數據檢查
- 檢查 dto 的合法性,比如文章標題要求唯一,dto.Title 就要驗證唯一性,中止操作並返回
- 檢查 entity 的合法性,比如文章已鎖定,就不允許編輯,要進行攔截,檢查不通過,中止操作並返回
- 使用
AutoMapper
將dto
的值更新到entity
- 進行其他關聯實體的更新
- 比如添加文章的編輯記錄
- 比如給當前操作人加積分
- 將
entity
的更新提交到資料庫
整個過程實現代碼如下:
/// <summary>
/// 更新文章信息
/// </summary>
/// <param name="dtos">包含更新信息的文章信息DTO信息</param>
/// <returns>業務操作結果</returns>
public virtual async Task<OperationResult> UpdatePosts(params PostInputDto[] dtos)
{
Check.Validate<PostInputDto, int>(dtos, nameof(dtos));
int count = 0;
foreach (PostInputDto dto in dtos)
{
Post entity = await PostRepository.GetAsync(dto.Id);
if (entity == null)
{
return new OperationResult(OperationResultType.QueryNull, $"編號為{dto.Id}的文章信息無法找到");
}
// todo:
// 在這裡要檢查 dto 的合法性,比如文章標題要求唯一,dto.Title 就要驗證唯一性
// 在這裡要檢查 entity 的合法性,比如文章已鎖定,就不允許編輯,要進行攔截
entity = dto.MapTo(entity);
// todo:
// 在這裡要進行其他實體的關聯更新,比如添加文章的編輯記錄
count += await PostRepository.UpdateAsync(entity);
}
if (count > 0)
{
return new OperationResult(OperationResultType.Success, $"{dtos.Length}個文章信息更新成功");
}
return OperationResult.NoChanged;
}
批量操作改進
這是個重覆性很大的繁瑣工作,整個流程中只有第2步和第4步是變化的,其餘步驟都相對固定。為了簡化這類操作,我們可以將第2、4步驟變化點封裝起來,使用 委托
將操作內容作為參數傳進來。
OSharp在 數據倉儲IRepository<TEntity, TKey>
中定義了關於這類 IInputDto
類型參數的實體批量操作API。
例如批量更新,實現如下:
/// <summary>
/// 非同步以DTO為載體批量更新實體
/// </summary>
/// <typeparam name="TEditDto">更新DTO類型</typeparam>
/// <param name="dtos">更新DTO信息集合</param>
/// <param name="checkAction">更新信息合法性檢查委托</param>
/// <param name="updateFunc">由DTO到實體的轉換委托</param>
/// <returns>業務操作結果</returns>
public virtual async Task<OperationResult> UpdateAsync<TEditDto>(ICollection<TEditDto> dtos,
Func<TEditDto, TEntity, Task> checkAction = null,
Func<TEditDto, TEntity, Task<TEntity>> updateFunc = null) where TEditDto : IInputDto<TKey>
{
List<string> names = new List<string>();
foreach (TEditDto dto in dtos)
{
try
{
TEntity entity = await _dbSet.FindAsync(dto.Id);
if (entity == null)
{
return new OperationResult(OperationResultType.QueryNull);
}
if (checkAction != null)
{
await checkAction(dto, entity);
}
entity = dto.MapTo(entity);
if (updateFunc != null)
{
entity = await updateFunc(dto, entity);
}
entity = CheckUpdate(entity)[0];
_dbContext.Update<TEntity, TKey>(entity);
}
catch (OsharpException e)
{
return new OperationResult(OperationResultType.Error, e.Message);
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
return new OperationResult(OperationResultType.Error, e.Message);
}
names.AddIfNotNull(GetNameValue(dto));
}
int count = await _dbContext.SaveChangesAsync(_cancellationTokenProvider.Token);
return count > 0
? new OperationResult(OperationResultType.Success,
names.Count > 0
? "信息“{0}”更新成功".FormatWith(names.ExpandAndToString())
: "{0}個信息更新成功".FormatWith(dtos.Count))
: new OperationResult(OperationResultType.NoChanged);
}
如上高亮代碼,此方法定義了 Func<TEditDto, TEntity, Task> checkAction
和 Func<TEditDto, TEntity, Task<TEntity>> updateFunc
兩個委托參數作為 更新前參數檢查
和 更新後關聯更新
的操作傳入方式,方法中是以 OsharpException
類型異常來作為中止信號的,如果需要在委托中中止操作,直接拋 OsharpException
異常即可。在調用時,即可極大簡化批量更新的操作,如上的更新代碼,簡化如下:
/// <summary>
/// 更新文章信息
/// </summary>
/// <param name="dtos">包含更新信息的文章信息DTO信息</param>
/// <returns>業務操作結果</returns>
public virtual async Task<OperationResult> UpdatePosts(params PostInputDto[] dtos)
{
Check.Validate<PostInputDto, int>(dtos, nameof(dtos));
return await PostRepository.UpdateAsync(dtos, async (dto, entity) =>
{
// todo:
// 在這裡要檢查 dto 的合法性,比如文章標題要求唯一,dto.Title 就要驗證唯一性
// 在這裡要檢查 entity 的合法性,比如文章已鎖定,就不允許編輯,要進行攔截
},
async (dto, entity) =>
{
// todo:
// 在這裡要進行其他實體的關聯更新,比如添加文章的編輯記錄
return entity;
});
}
如果沒有必要做額外的 更新前檢查 和更新後的 關聯更新,上面的批量更新可以簡化到極致:
/// <summary>
/// 更新文章信息
/// </summary>
/// <param name="dtos">包含更新信息的文章信息DTO信息</param>
/// <returns>業務操作結果</returns>
public virtual async Task<OperationResult> UpdatePosts(params PostInputDto[] dtos)
{
Check.Validate<PostInputDto, int>(dtos, nameof(dtos));
return await PostRepository.UpdateAsync(dtos);
}
服務層的事務管理
事務開啟與重用
OSharp的數據層在一次業務處理請求中遇到數據的 新增、更新、刪除
操作並第一次執行 SaveChanges
操作時,會自動開啟手動事務,以後再次執行 SaveChanges
操作時,會直接使用 同一連接對象 的現有事務,以保證一次業務請求的操作都自在一個事務內。
public override int SaveChanges()
{
// ...
//開啟或使用現有事務
BeginOrUseTransaction();
int count = base.SaveChanges();
// ...
return count;
}
事務提交
為了方便事務管理和不同的服務層之間的事務同步,OSharp框架預設的事務提交是在 API 層通過 MVC 的 UnitOfWorkAttribute 特性來提交的。
/// <summary>
/// 新用戶註冊
/// </summary>
/// <param name="dto">註冊信息</param>
/// <returns>JSON操作結果</returns>
[HttpPost]
[ServiceFilter(typeof(UnitOfWorkAttribute))]
[ModuleInfo]
[Description("用戶註冊")]
public async Task<AjaxResult> Register(RegisterDto dto)
{
// ...
}
當然,你也可以不在 API 層標註 [UnitOfWorkAttribute]
,而是在需要的時候通過 IUnitOfWork.Commit()
手動提交事務
IUnitOfWork unitOfWork = HttpContext.RequestServices.GetUnitOfWork<User, int>();
unitOfWork.Commit();
業務服務事件訂閱與發佈
業務服務事件,是通過 事件匯流排EventBus
來實現的,OSharp構建了一個簡單的事件匯流排基礎建設,可以很方便地訂閱和發佈業務事件。
訂閱事件
訂閱事件很簡單,只需要定義一組配套的 事件數據EventData
和相應的 事件處理器EventHandler
,即可完成事件訂閱的工作。
IEventData
事件數據EventData
是業務服務發佈事件時向事件匯流排傳遞的數據,每一種業務,都有特定的事件數據,一個事件數據可觸發多個事件處理器
定義一個事件數據,需要實現 IEventData
介面
/// <summary>
/// 定義事件數據,所有事件都要實現該介面
/// </summary>
public interface IEventData
{
/// <summary>
/// 獲取 事件編號
/// </summary>
Guid Id { get; }
/// <summary>
/// 獲取 事件發生的時間
/// </summary>
DateTime EventTime { get; }
/// <summary>
/// 獲取或設置 事件源,觸發事件的對象
/// </summary>
object EventSource { get; set; }
}
EventDataBase
為了方便 事件數據 的定義,OSharp定義了一個通用事件數據基類EventDataBase
,繼承此基類,只需要添加事件觸發需要的業務數據即可
/// <summary>
/// 事件源數據信息基類
/// </summary>
public abstract class EventDataBase : IEventData
{
/// <summary>
/// 初始化一個<see cref="EventDataBase"/>類型的新實例
/// </summary>
protected EventDataBase()
{
Id = Guid.NewGuid();
EventTime = DateTime.Now;
}
/// <summary>
/// 獲取 事件編號
/// </summary>
public Guid Id { get; }
/// <summary>
/// 獲取 事件發生時間
/// </summary>
public DateTime EventTime { get; }
/// <summary>
/// 獲取或設置 觸發事件的對象
/// </summary>
public object EventSource { get; set; }
}
IEventHandler
業務事件的處理邏輯,是通過 事件處理器 EventHandler
來實現的,事件處理器應遵從 單一職責
原則,一個處理器只做一件事,業務服務層發佈一項 事件數據,可觸發多個 事件處理器
/// <summary>
/// 定義事件處理器,所有事件處理都要實現該介面
/// EventBus中,Handler的調用是同步執行的,如果需要觸發就不管的非同步執行,可以在實現EventHandler的Handle邏輯時使用Task.Run
/// </summary>
[IgnoreDependency]
public interface IEventHandler
{
/// <summary>
/// 是否可處理指定事件
/// </summary>
/// <param name="eventData">事件源數據</param>
/// <returns>是否可處理</returns>
bool CanHandle(IEventData eventData);
/// <summary>
/// 事件處理
/// </summary>
/// <param name="eventData">事件源數據</param>
void Handle(IEventData eventData);
/// <summary>
/// 非同步事件處理
/// </summary>
/// <param name="eventData">事件源數據</param>
/// <param name="cancelToken">非同步取消標識</param>
/// <returns></returns>
Task HandleAsync(IEventData eventData, CancellationToken cancelToken = default(CancellationToken));
}
泛型事件處理器
/// <summary>
/// 定義泛型事件處理器
/// EventBus中,Handler的調用是同步執行的,如果需要觸發就不管的非同步執行,可以在實現EventHandler的Handle邏輯時使用Task.Run
/// </summary>
/// <typeparam name="TEventData">事件源數據</typeparam>
[IgnoreDependency]
public interface IEventHandler<in TEventData> : IEventHandler where TEventData : IEventData
{
/// <summary>
/// 事件處理
/// </summary>
/// <param name="eventData">事件源數據</param>
void Handle(TEventData eventData);
/// <summary>
/// 非同步事件處理
/// </summary>
/// <param name="eventData">事件源數據</param>
/// <param name="cancelToken">非同步取消標識</param>
Task HandleAsync(TEventData eventData, CancellationToken cancelToken = default(CancellationToken));
}
EventHandlerBase
同樣的,為了方便 事件處理器 的定義,OSharp定義了一個通用的事件處理器基類EventHandlerBase<TEventData>
,繼承此基類,只需要實現核心的事件處理邏輯即可
/// <summary>
/// 事件處理器基類
/// </summary>
public abstract class EventHandlerBase<TEventData> : IEventHandler<TEventData> where TEventData : IEventData
{
/// <summary>
/// 是否可處理指定事件
/// </summary>
/// <param name="eventData">事件源數據</param>
/// <returns>是否可處理</returns>
public virtual bool CanHandle(IEventData eventData)
{
return eventData.GetType() == typeof(TEventData);
}
/// <summary>
/// 事件處理
/// </summary>
/// <param name="eventData">事件源數據</param>
public virtual void Handle(IEventData eventData)
{
if (!CanHandle(eventData))
{
return;
}
Handle((TEventData)eventData);
}
/// <summary>
/// 非同步事件處理
/// </summary>
/// <param name="eventData">事件源數據</param>
/// <param name="cancelToken">非同步取消標識</param>
/// <returns></returns>
public virtual Task HandleAsync(IEventData eventData, CancellationToken cancelToken = default(CancellationToken))
{
if (!CanHandle(eventData))
{
return Task.FromResult(0);
}
return HandleAsync((TEventData)eventData, cancelToken);
}
/// <summary>
/// 事件處理
/// </summary>
/// <param name="eventData">事件源數據</param>
public abstract void Handle(TEventData eventData);
/// <summary>
/// 非同步事件處理
/// </summary>
/// <param name="eventData">事件源數據</param>
/// <param name="cancelToken">非同步取消標識</param>
/// <returns>是否成功</returns>
public virtual Task HandleAsync(TEventData eventData, CancellationToken cancelToken = default(CancellationToken))
{
return Task.Run(() => Handle(eventData), cancelToken);
}
}
發佈事件
事件的發佈,就相當簡單了,只需要實例化一個事件數據EventData
的實例,然後通過IEventBus.Publish(eventData)
即可發佈事件,觸發該EventData
的所有訂閱處理器
XXXEventData eventData = new XXXEventData()
{
// ...
};
EventBus.Publish(eventData);
博客模塊的業務事件實現
回到我們的 Liuliu.Blogs
項目,根據 <業務模塊設計#博客業務需求分析> 的需求分析的第二條,審核博客之後需要發郵件通知用戶,發郵件屬於審核博客業務計劃外的需求,使用 業務事件 來實現正當其時。
- 審核博客業務事件數據
/// <summary>
/// 審核博客事件數據
/// </summary>
public class VerifyBlogEventData : EventDataBase
{
/// <summary>
/// 獲取或設置 博客名稱
/// </summary>
public string BlogName { get; set; }
/// <summary>
/// 獲取或設置 用戶名
/// </summary>
public string UserName { get; set; }
/// <summary>
/// 獲取或設置 審核是否通過
/// </summary>
public bool IsEnabled { get; set; }
}
- 審核博客業務事件處理器
/// <summary>
/// 審核博客事件處理器
/// </summary>
public class VerifyBlogEventHandler : EventHandlerBase<VerifyBlogEventData>
{
private readonly ILogger _logger;
/// <summary>
/// 初始化一個<see cref="VerifyBlogEventHandler"/>類型的新實例
/// </summary>
public VerifyBlogEventHandler(IServiceProvider serviceProvider)
{
_logger = serviceProvider.GetService<ILoggerFactory>().CreateLogger<VerifyBlogEventHandler>();
}
/// <summary>事件處理</summary>
/// <param name="eventData">事件源數據</param>
public override void Handle(VerifyBlogEventData eventData)
{
_logger.LogInformation(
$"觸發 審核博客事件處理器,用戶“{eventData.UserName}”的博客“{eventData.BlogName}”審核結果:{(eventData.IsEnabled ? "通過" : "未通過")}");
}
}
博客模塊的服務實現
回到我們的 Liuliu.Blogs
項目,根據 <業務模塊設計#服務層> 的需求分析,綜合使用OSharp框架提供的基礎建設,博客模塊的業務服務實現如下:
BlogsService.cs
/// <summary>
/// 業務服務實現:博客模塊
/// </summary>
public partial class BlogsService : IBlogsContract
{
private readonly IServiceProvider _serviceProvider;
/// <summary>
/// 初始化一個<see cref="BlogsService"/>類型的新實例
/// </summary>
public BlogsService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
/// <summary>
/// 獲取 博客倉儲對象
/// </summary>
protected IRepository<Blog, int> BlogRepository => _serviceProvider.GetService<IRepository<Blog, int>>();
/// <summary>
/// 獲取 文章倉儲對象
/// </summary>
protected IRepository<Post, int> PostRepository => _serviceProvider.GetService<IRepository<Post, int>>();
/// <summary>
/// 獲取 用戶倉儲對象
/// </summary>
protected IRepository<User, int> UserRepository => _serviceProvider.GetService<IRepository<User, int>>();
}
BlogsService.Blog.cs
public partial class BlogsService
{
/// <summary>
/// 獲取 博客信息查詢數據集
/// </summary>
public virtual IQueryable<Blog> Blogs => BlogRepository.Query();
/// <summary>
/// 檢查博客信息是否存在
/// </summary>
/// <param name="predicate">檢查謂語表達式</param>
/// <param name="id">更新的博客信息編號</param>
/// <returns>博客信息是否存在</returns>
public virtual Task<bool> CheckBlogExists(Expression<Func<Blog, bool>> predicate, int id = 0)
{
Check.NotNull(predicate, nameof(predicate));
return BlogRepository.CheckExistsAsync(predicate, id);
}
/// <summary>
/// 申請博客信息
/// </summary>
/// <param name="dto">申請博客信息DTO信息</param>
/// <returns>業務操作結果</returns>
public virtual async Task<OperationResult> ApplyForBlog(BlogInputDto dto)
{
Check.Validate(dto, nameof(dto));
// 博客是以當前用戶的身份來申請的
ClaimsPrincipal principal = _serviceProvider.GetCurrentUser();
if (principal == null || !principal.Identity.IsAuthenticated)
{
return new OperationResult(OperationResultType.Error, "用戶未登錄或登錄已失效");
}
int userId = principal.Identity.GetUserId<int>();
User user = await UserRepository.GetAsync(userId);
if (user == null)
{
return new OperationResult(OperationResultType.QueryNull, $"編號為“{userId}”的用戶信息不存在");
}
Blog blog = BlogRepository.TrackQuery(m => m.UserId == userId).FirstOrDefault();
if (blog != null)
{
return new OperationResult(OperationResultType.Error, "當前用戶已開通博客,不能重覆申請");
}
if (await CheckBlogExists(m => m.Url == dto.Url))
{
return new OperationResult(OperationResultType.Error, $"Url 為“{dto.Url}”的博客已存在,不能重覆添加");
}
blog = dto.MapTo<Blog>();
blog.UserId = userId;
int count = await BlogRepository.InsertAsync(blog);
return count > 0
? new OperationResult(OperationResultType.Success, "博客申請成功")
: OperationResult.NoChanged;
}
/// <summary>
/// 審核博客信息
/// </summary>
/// <param name="id">博客編號</param>
/// <param name="isEnabled">是否通過</param>
/// <returns>業務操作結果</returns>
public virtual async Task<OperationResult> VerifyBlog(int id, bool isEnabled)
{
Blog blog = await BlogRepository.GetAsync(id);
if (blog == null)
{
return new OperationResult(OperationResultType.QueryNull, $"編號為“{id}”的博客信息不存在");
}
// 更新博客
blog.IsEnabled = isEnabled;
int count = await BlogRepository.UpdateAsync(blog);
User user = await UserRepository.GetAsync(blog.UserId);
if (user == null)
{
return new OperationResult(OperationResultType.QueryNull, $"編號為“{blog.UserId}”的用戶信息不存在");
}
// 如果開通博客,給用戶開通博主身份
if (isEnabled)
{
// 查找博客主的角色,博主角色名可由配置系統獲得
const string roleName = "博主";
// 用於CUD操作的實體,要用 TrackQuery 方法來查詢出需要的數據,不能用 Query,因為 Query 會使用 AsNoTracking
Role role = RoleRepository.TrackQuery(m => m.Name == roleName).FirstOrDefault();
if (role == null)
{
return new OperationResult(OperationResultType.QueryNull, $"名稱為“{roleName}”的角色信息不存在");
}
UserRole userRole = UserRoleRepository.TrackQuery(m => m.UserId == user.Id && m.RoleId == role.Id)
.FirstOrDefault();
if (userRole == null)
{
userRole = new UserRole() { UserId = user.Id, RoleId = role.Id, IsLocked = false };
count += await UserRoleRepository.InsertAsync(userRole);
}
}
OperationResult result = count > 0
? new OperationResult(OperationResultType.Success, $"博客“{blog.Display}”審核 {(isEnabled ? "通過" : "未通過")}")
: OperationResult.NoChanged;
if (result.Succeeded)
{
VerifyBlogEventData eventData = new VerifyBlogEventData()
{
BlogName = blog.Display,
UserName = user.NickName,
IsEnabled = isEnabled
};
EventBus.Publish(eventData);
}
return result;
}
/// <summary>
/// 更新博客信息
/// </summary>
/// <param name="dtos">包含更新信息的博客信息DTO信息</param>
/// <returns>業務操作結果</returns>
public virtual Task<OperationResult> UpdateBlogs(params BlogInputDto[] dtos)
{
return BlogRepository.UpdateAsync(dtos, async (dto, entity) =>
{
if (await BlogRepository.CheckExistsAsync(m => m.Url == dto.Url, dto.Id))
{
throw new OsharpException($"Url為“{dto.Url}”的博客已存在,不能重覆");
}
});
}
/// <summary>
/// 刪除博客信息
/// </summary>
/// <param name="ids">要刪除的博客信息編號</param>
/// <returns>業務操作結果</returns>
public virtual Task<OperationResult> DeleteBlogs(params int[] ids)
{
return BlogRepository.DeleteAsync(ids, entity =>
{
if (PostRepository.Query(m => m.BlogId == entity.Id).Any())
{
throw new OsharpException($"博客“{entity.Display}”中還有文章未刪除,請先刪除所有文章,再刪除博客");
}
return Task.FromResult(0);
});
}
}
BlogsService.Post.cs
public partial class BlogsService
{
/// <summary>
/// 獲取 文章信息查詢數據集
/// </summary>
public virtual IQueryable<Post> Posts => PostRepository.Query();
/// <summary>
/// 檢查文章信息是否存在
/// </summary>
/// <param name="predicate">檢查謂語表達式</param>
/// <param name="id">更新的文章信息編號</param>
/// <returns>文章信息是否存在</returns>
public virtual Task<bool> CheckPostExists(Expression<Func<Post, bool>> predicate, int id = 0)
{
Check.NotNull(predicate, nameof(predicate));
return PostRepository.CheckExistsAsync(predicate, id);
}
/// <summary>
/// 添加文章信息
/// </summary>
/// <param name="dtos">要添加的文章信息DTO信息</param>
/// <returns>業務操作結果</returns>
public virtual async Task<OperationResult> CreatePosts(params PostInputDto[] dtos)
{
Check.Validate<PostInputDto, int>(dtos, nameof(dtos));
if (dtos.Length == 0)
{
return OperationResult.NoChanged;
}
// 文章是以當前用戶身份來添加的
ClaimsPrincipal principal = _serviceProvider.GetCurrentUser();
if (principal == null || !principal.Identity.IsAuthenticated)
{
throw new OsharpException("用戶未登錄或登錄已失效");
}
// 檢查當前用戶的博客狀態
int userId = principal.Identity.GetUserId<int>();
Blog blog = BlogRepository.TrackQuery(m => m.UserId == userId).FirstOrDefault();
if (blog == null || !blog.IsEnabled)
{
throw new OsharpException("當前用戶的博客未開通,無法添加文章");
}
// 沒有前置檢查,checkAction為null
return await PostRepository.InsertAsync(dtos, null, (dto, entity) =>
{
// 給新建的文章關聯博客和作者
entity.BlogId = blog.Id;
entity.UserId = userId;
return Task.FromResult(entity);
});
}
/// <summary>
/// 更新文章信息
/// </summary>
/// <param name="dtos">包含更新信息的文章信息DTO信息</param>
/// <returns>業務操作結果</returns>
public virtual Task<OperationResult> UpdatePosts(params PostInputDto[] dtos)
{
Check.Validate<PostInputDto, int>(dtos, nameof(dtos));
return PostRepository.UpdateAsync(dtos);
}
/// <summary>
/// 刪除文章信息
/// </summary>
/// <param name="ids">要刪除的文章信息編號</param>
/// <returns>業務操作結果</returns>
public virtual Task<OperationResult> DeletePosts(params int[] ids)
{
Check.NotNull(ids, nameof(ids));
return PostRepository.DeleteAsync(ids);
}
}
模塊入口 BlogsPack
模塊入口基類
非AspNetCore模塊基類 OsharpPack
前面多次提到,每個Pack模塊都是繼承自一個 模塊基類OsharpPack
,這個基類用於定義 模塊初始化UsePack
過程中未涉及 AspNetCore
環境的模塊。
/// <summary>
/// OSharp模塊基類
/// </summary>
public abstract class OsharpPack
{
/// <summary>
/// 獲取 模塊級別,級別越小越先啟動
/// </summary>
public virtual PackLevel Level => PackLevel.Business;
/// <summary>
/// 獲取 模塊啟動順序,模塊啟動的順序先按級別啟動,同一級別內部再按此順序啟動,
/// 級別預設為0,表示無依賴,需要在同級別有依賴順序的時候,再重寫為>0的順序值
/// </summary>
public virtual int Order => 0;
/// <summary>
/// 獲取 是否已可用
/// </summary>
public bool IsEnabled { get; protected set; }
/// <summary>
/// 將模塊服務添加到依賴註入服務容器中
/// </summary>
/// <param name="services">依賴註入服務容器</param>
/// <returns></returns>
public virtual IServiceCollection AddServices(IServiceCollection services)
{
return services;
}
/// <summary>
/// 應用模塊服務
/// </summary>
/// <param name="provider">服務提供者</param>
public virtual void UsePack(IServiceProvider provider)
{
IsEnabled = true;
}
/// <summary>
/// 獲取當前模塊的依賴模塊類型
/// </summary>
/// <returns></returns>
internal Type[] GetDependPackTypes(Type packType = null)
{
// ...
}
}
模塊基類OsharpPack
定義了兩個可重寫屬性:
PackLevel
:模塊級別,級別越小越先啟動
模塊級別按 模塊 在框架中不同的功能層次,定義瞭如下幾個級別:
/// <summary>
/// 模塊級別,級別越核心,優先啟動
/// </summary>
public enum PackLevel
{
/// <summary>
/// 核心級別,表示系統的核心模塊,
/// 這些模塊不涉及第三方組件,在系統運行中是不可替換的,核心模塊將始終載入
/// </summary>
Core = 1,
/// <summary>
/// 框架級別,表示涉及第三方組件的基礎模塊
/// </summary>
Framework = 10,
/// <summary>
/// 應用級別,表示涉及應用數據的基礎模塊
/// </summary>
Application = 20,
/// <summary>
/// 業務級別,表示涉及真實業務處理的模塊
/// </summary>
Business = 30
}
Order
:級別內模塊啟動順序,模塊啟動的順序先按級別啟動,同一級別內部再按此順序啟動,級別預設為 0,表示無依賴,需要在同級別有依賴順序的時候,再重寫為 >0 的順序值
同時,模塊基類 還定義了兩個方法:
AddServices
:用於將模塊內定義的服務註入到 依賴註入服務容器 中。UsePack
:用於使用服務對當前模塊進行初始化。
AspNetCore模塊基類 AspOsharpPack
AspOsharpPack
基類繼承了 OsharpPack
,添加了一個對 IApplicationBuilder
支持的 UsePack
方法,用於實現與 AspNetCore
關聯的模塊初始化工作,例如 Mvc模塊 初始化的時候需要應用中間件:app.UseMvcWithAreaRoute();
/// <summary>
/// 基於AspNetCore環境的Pack模塊基類
/// </summary>
public abstract class AspOsharpPack : OsharpPack
{
/// <summary>
/// 應用AspNetCore的服務業務
/// </summary>
/// <param name="app">Asp應用程式構建器</param>
public virtual void UsePack(IApplicationBuilder app)
{
base.UsePack(app.ApplicationServices);
}
}
博客模塊的模塊入口實現
回到我們的 Liuliu.Blogs
項目,我們來實現投票模塊的模塊入口類 BlogsPack
:
- 博客模塊屬於業務模塊,因此
PackLevel
設置為Business
- 博客模塊的啟動順序無需重寫,保持 0 即可
- 將 博客業務服務 註冊到 服務容器中
- 無甚初始化業務
實現代碼如下:
/// <summary>
/// 博客模塊
/// </summary>
public class BlogsPack : OsharpPack
{
/// <summary>
/// 獲取 模塊級別,級別越小越先啟動
/// </summary>
public override PackLevel Level { get; } = PackLevel.Business;
/// <summary>將模塊服務添加到依賴註入服務容器中</summary>
/// <param name="services">依賴註入服務容器</param>
/// <returns></returns>
public override IServiceCollection AddServices(IServiceCollection services)
{
services.TryAddScoped<IBlogsContract, BlogsService>();
return services;
}
}
至此,博客模塊的服務層實現完畢。