[開源]OSharpNS 步步為營系列 - 3. 添加業(yè)務(wù)服務(wù)層
什么是OSharp
OSharpNS全稱OSharp Framework with .NetStandard2.0,是一個(gè)基于.NetStandard2.0開發(fā)的一個(gè).NetCore快速開發(fā)框架。這個(gè)框架使用最新穩(wěn)定版的.NetCore SDK(當(dāng)前是.NET Core 2.2),對 AspNetCore 的配置、依賴注入、日志、緩存、實(shí)體框架、Mvc(WebApi)、身份認(rèn)證、權(quán)限授權(quán)等模塊進(jìn)行更高一級的自動化封裝,并規(guī)范了一套業(yè)務(wù)實(shí)現(xiàn)的代碼結(jié)構(gòu)與操作流程,使 .Net Core 框架更易于應(yīng)用到實(shí)際項(xiàng)目開發(fā)中。
- 開源地址: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
概述
一個(gè)模塊的服務(wù)層,主要負(fù)責(zé)如下幾個(gè)方面的工作:
- 向 API層 提供各個(gè)實(shí)體的數(shù)據(jù)查詢的
IQueryable<T>類型的數(shù)據(jù)源 - 接收 API層 傳入的
IInputDto參數(shù),完成實(shí)體的新增、更新、刪除等業(yè)務(wù)操作 - 接收 API層 傳入的參數(shù),處理各種
模塊級別的綜合業(yè)務(wù) - 處理完業(yè)務(wù)之后,將數(shù)據(jù)通過 數(shù)據(jù)倉儲
IRepository更新到數(shù)據(jù)庫 - 向
事件總線模塊發(fā)布業(yè)務(wù)處理事件,觸發(fā)訂閱的業(yè)務(wù)事件 - 向 API 層返回業(yè)務(wù)操作結(jié)果
整個(gè)過程如下圖所示:

服務(wù)層代碼布局
服務(wù)層代碼布局分析
一個(gè)業(yè)務(wù)模塊,是負(fù)責(zé)完成一系列功能的,這些功能相互之間具有密切的關(guān)聯(lián)性,所以對于一個(gè)模塊來說,業(yè)務(wù)服務(wù)是一個(gè)整體,不應(yīng)把他們再按單個(gè)實(shí)體拆分開來。
OSharp 的業(yè)務(wù)模塊代碼結(jié)構(gòu)設(shè)計(jì),也是根據(jù)這一原則來設(shè)計(jì)的。設(shè)計(jì)規(guī)則如下:
-
服務(wù)接口
IBlogsContract:一個(gè)模塊的業(yè)務(wù)服務(wù)共享一個(gè)服務(wù)接口,接口中包含模塊的綜合業(yè)務(wù)服務(wù),也包含模塊的各個(gè)實(shí)體的查詢數(shù)據(jù)集、新增、更新、刪除等自有業(yè)務(wù)服務(wù)。 -
服務(wù)實(shí)現(xiàn)
BlogsService:服務(wù)實(shí)現(xiàn)使用 分部類partial設(shè)計(jì),例如本例中的博客模塊業(yè)務(wù),文件拆分如下:BlogsService.cs:博客模塊服務(wù)實(shí)現(xiàn)類的主文件,負(fù)責(zé)各實(shí)體的倉儲服務(wù)注入,輔助服務(wù)注入,模塊綜合業(yè)務(wù)實(shí)現(xiàn)BlogsService.Blog.cs:博客模塊服務(wù)的博客實(shí)體服務(wù)實(shí)現(xiàn)類,負(fù)責(zé)博客實(shí)體的查詢數(shù)據(jù)集、增改刪業(yè)務(wù)實(shí)現(xiàn)BlogsService.Post.cs:博客模塊服務(wù)的文章實(shí)體服務(wù)實(shí)現(xiàn)類,負(fù)責(zé)文章實(shí)體的查詢數(shù)據(jù)集、增改刪業(yè)務(wù)實(shí)現(xiàn)
-
模塊入口
BlogsPack:定義模塊的級別、啟動順序、執(zhí)行服務(wù)添加、模塊初始化等功能
綜上,服務(wù)層代碼布局如下所示:
src # 源代碼文件夾
└─Liuliu.Blogs.Core # 項(xiàng)目核心工程
└─Blogs # 博客模塊文件夾
├─Events # 業(yè)務(wù)事件文件夾
│ ├─VerifyBlogEventData.cs # 審核博客事件數(shù)據(jù)
│ └─VerifyBlogEventHandler.cs # 審核博客事件處理器
├─BlogsPack.cs # 博客模塊入口類
├─BlogsService.cs # 博客服務(wù)類
├─BlogsService.Blog.cs # 博客模塊-博客服務(wù)類
├─BlogsService.Post.cs # 博客模塊-文章服務(wù)類
└─IBlogsContract.cs # 博客模塊服務(wù)接口
服務(wù)接口 IBlogsContract
接口定義分析
數(shù)據(jù)查詢
對于數(shù)據(jù)查詢,業(yè)務(wù)層只向 API 層開放一個(gè) IQueryable<TEntity> 的查詢數(shù)據(jù)集。原則上,服務(wù)層不實(shí)現(xiàn) 純數(shù)據(jù)查詢(例如 用于列表分頁數(shù)據(jù)、下拉菜單選項(xiàng) 等數(shù)據(jù),不涉及數(shù)據(jù)變更的查詢操作) 的服務(wù),所有的 純數(shù)據(jù)查詢 都在 API層 按需要進(jìn)行查詢。具體分析請看 >>數(shù)據(jù)查詢應(yīng)該在哪做>>
額外的,根據(jù)一定條件判斷一個(gè)數(shù)據(jù)是否存在 這種需求經(jīng)常會用到(例如在新增或修改一個(gè)要求唯一的字符串時(shí),需要異步檢查輸入的字符串是否已存在),因此設(shè)計(jì)一個(gè) 檢查實(shí)體是否存在CheckEntityExists 的服務(wù)很有必要。
!!!node
對于新增、更新、刪除操作,除非很確定一次只操作一條記錄除外,為了支持可能的批量操作,設(shè)計(jì)上都應(yīng)把服務(wù)層的 增改刪 操作設(shè)計(jì)為數(shù)組型參數(shù)的批量操作,同時(shí)使用 params 關(guān)鍵字使操作支持單個(gè)數(shù)據(jù)操作。
數(shù)據(jù)變更
對于每一個(gè)實(shí)體,服務(wù)層按 業(yè)務(wù)需求分析 的要求定義必要的 新增、更新、刪除 等操作,OSharp框架定義了一個(gè) 業(yè)務(wù)操作結(jié)果信息類 OperationResult 來封裝業(yè)務(wù)操作結(jié)果,這個(gè)結(jié)果可以返回 操作結(jié)果類型(成功/錯(cuò)誤/未變化/不存在/驗(yàn)證失敗)、返回消息、返回附加數(shù)據(jù) 等豐富的信息,API層 接受操作結(jié)果后可進(jìn)行相應(yīng)的處理。
博客模塊的接口定義
回到我們的 Liuliu.Blogs 項(xiàng)目,根據(jù) <業(yè)務(wù)模塊設(shè)計(jì)#服務(wù)層> 的需求分析,我們需要給 博客Blog 實(shí)體定義 申請開通、開通審核、更新、刪除 服務(wù),給 文章Post 實(shí)體類定義 新增、更新、刪除 服務(wù)。
接口定義如下:
/// <summary>
/// 業(yè)務(wù)契約接口:博客模塊
/// </summary>
public interface IBlogsContract
{
#region 博客信息業(yè)務(wù)
/// <summary>
/// 獲取 博客信息查詢數(shù)據(jù)集
/// </summary>
IQueryable<Blog> Blogs { get; }
/// <summary>
/// 檢查博客信息是否存在
/// </summary>
/// <param name="predicate">檢查謂語表達(dá)式</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>業(yè)務(wù)操作結(jié)果</returns>
Task<OperationResult> ApplyForBlog(BlogInputDto dto);
/// <summary>
/// 審核博客信息
/// </summary>
/// <param name="id">博客編號</param>
/// <param name="isEnabled">是否通過</param>
/// <returns>業(yè)務(wù)操作結(jié)果</returns>
Task<OperationResult> VerifyBlog(int id, bool isEnabled);
/// <summary>
/// 更新博客信息
/// </summary>
/// <param name="dtos">包含更新信息的博客信息DTO信息</param>
/// <returns>業(yè)務(wù)操作結(jié)果</returns>
Task<OperationResult> UpdateBlogs(params BlogInputDto[] dtos);
/// <summary>
/// 刪除博客信息
/// </summary>
/// <param name="ids">要?jiǎng)h除的博客信息編號</param>
/// <returns>業(yè)務(wù)操作結(jié)果</returns>
Task<OperationResult> DeleteBlogs(params int[] ids);
#endregion
#region 文章信息業(yè)務(wù)
/// <summary>
/// 獲取 文章信息查詢數(shù)據(jù)集
/// </summary>
IQueryable<Post> Posts { get; }
/// <summary>
/// 檢查文章信息是否存在
/// </summary>
/// <param name="predicate">檢查謂語表達(dá)式</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>業(yè)務(wù)操作結(jié)果</returns>
Task<OperationResult> CreatePosts(params PostInputDto[] dtos);
/// <summary>
/// 更新文章信息
/// </summary>
/// <param name="dtos">包含更新信息的文章信息DTO信息</param>
/// <returns>業(yè)務(wù)操作結(jié)果</returns>
Task<OperationResult> UpdatePosts(params PostInputDto[] dtos);
/// <summary>
/// 刪除文章信息
/// </summary>
/// <param name="ids">要?jiǎng)h除的文章信息編號</param>
/// <returns>業(yè)務(wù)操作結(jié)果</returns>
Task<OperationResult> DeletePosts(params int[] ids);
#endregion
}
服務(wù)實(shí)現(xiàn) BlogsService
依賴服務(wù)注入方式分析
服務(wù)層的業(yè)務(wù)實(shí)現(xiàn),通過向服務(wù)實(shí)現(xiàn)類注入 數(shù)據(jù)倉儲IRepository<TEntity, TKey> 對象來獲得向數(shù)據(jù)庫存取數(shù)據(jù)的能力。根據(jù) .NetCore 的依賴注入使用原則,常規(guī)的做法是在服務(wù)實(shí)現(xiàn)類的 構(gòu)造函數(shù) 進(jìn)行依賴服務(wù)的注入。形如:
/// <summary>
/// 業(yè)務(wù)服務(wù)實(shí)現(xiàn):博客模塊
/// </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>
/// 初始化一個(gè)<see cref="BlogsService"/>類型的新實(shí)例
/// </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;
}
}
構(gòu)造函數(shù)注入帶來的性能影響
每個(gè)倉儲都使用構(gòu)造函數(shù)注入的話,如果模塊的業(yè)務(wù)比較復(fù)雜,涉及的實(shí)體比較多(比如十幾個(gè)實(shí)體是很經(jīng)常的事),就會造成每次實(shí)例化 BlogsService 類的實(shí)例的時(shí)候,都需要去實(shí)例化很多個(gè)依賴服務(wù),而實(shí)際上 一次業(yè)務(wù)執(zhí)行只執(zhí)行服務(wù)中的某個(gè)方法,可能也就用到其中的一兩個(gè)依賴服務(wù),這就造成了很多不必要的額外工作,也就是性能損耗。
依賴服務(wù)注入的性能優(yōu)化
如果 不考慮業(yè)務(wù)服務(wù)的可測試性(單元測試通常需要Mock依賴服務(wù))的話,在構(gòu)造函數(shù)中只注入 IServiceProvider 實(shí)例,然后在業(yè)務(wù)代碼中使用 serviceProvider.GetService<T>() 的方式來 按需獲取 依賴服務(wù)的實(shí)例,是比較經(jīng)濟(jì)的方式。則服務(wù)實(shí)現(xiàn)變?yōu)槿缦滤荆?/p>
/// <summary>
/// 業(yè)務(wù)服務(wù)實(shí)現(xiàn):博客模塊
/// </summary>
public class BlogsService : IBlogsContract
{
private readonly IServiceProvider _serviceProvider;
/// <summary>
/// 初始化一個(gè)<see cref="BlogsService"/>類型的新實(shí)例
/// </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>();
}
各個(gè)依賴服務(wù)改為屬性的存在方式,并且可訪問性為 protected,這就保證了依賴服務(wù)的安全性。依賴服務(wù)使用 serviceProvider.GetService<T>() 的方式創(chuàng)建實(shí)例,可以做到 按需創(chuàng)建,達(dá)到性能優(yōu)化的目的。
增改刪操作的簡化
常規(guī)批量操作的弊端
直接通過 數(shù)據(jù)倉儲IRepository<TEntity, TKey> 實(shí)現(xiàn)數(shù)據(jù)的增改刪的批量操作,總免不了要使用循環(huán)來遍歷傳進(jìn)來的多個(gè)InputDto,例如文章的更新操作,以下幾個(gè)步驟是免不了的:
- 由
dto.Id查找出相應(yīng)的文章實(shí)體entity,如果不存在,中止操作并返回 - 進(jìn)行更新前的數(shù)據(jù)檢查
- 檢查 dto 的合法性,比如文章標(biāo)題要求唯一,dto.Title 就要驗(yàn)證唯一性,中止操作并返回
- 檢查 entity 的合法性,比如文章已鎖定,就不允許編輯,要進(jìn)行攔截,檢查不通過,中止操作并返回
- 使用
AutoMapper將dto的值更新到entity - 進(jìn)行其他關(guān)聯(lián)實(shí)體的更新
- 比如添加文章的編輯記錄
- 比如給當(dāng)前操作人加積分
- 將
entity的更新提交到數(shù)據(jù)庫
整個(gè)過程實(shí)現(xiàn)代碼如下:
/// <summary>
/// 更新文章信息
/// </summary>
/// <param name="dtos">包含更新信息的文章信息DTO信息</param>
/// <returns>業(yè)務(wù)操作結(jié)果</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 的合法性,比如文章標(biāo)題要求唯一,dto.Title 就要驗(yàn)證唯一性
// 在這里要檢查 entity 的合法性,比如文章已鎖定,就不允許編輯,要進(jìn)行攔截
entity = dto.MapTo(entity);
// todo:
// 在這里要進(jìn)行其他實(shí)體的關(guān)聯(lián)更新,比如添加文章的編輯記錄
count += await PostRepository.UpdateAsync(entity);
}
if (count > 0)
{
return new OperationResult(OperationResultType.Success, $"{dtos.Length}個(gè)文章信息更新成功");
}
return OperationResult.NoChanged;
}
批量操作改進(jìn)
這是個(gè)重復(fù)性很大的繁瑣工作,整個(gè)流程中只有第2步和第4步是變化的,其余步驟都相對固定。為了簡化這類操作,我們可以將第2、4步驟變化點(diǎn)封裝起來,使用 委托 將操作內(nèi)容作為參數(shù)傳進(jìn)來。
OSharp在 數(shù)據(jù)倉儲IRepository<TEntity, TKey> 中定義了關(guān)于這類 IInputDto 類型參數(shù)的實(shí)體批量操作API。
例如批量更新,實(shí)現(xiàn)如下:
/// <summary>
/// 異步以DTO為載體批量更新實(shí)體
/// </summary>
/// <typeparam name="TEditDto">更新DTO類型</typeparam>
/// <param name="dtos">更新DTO信息集合</param>
/// <param name="checkAction">更新信息合法性檢查委托</param>
/// <param name="updateFunc">由DTO到實(shí)體的轉(zhuǎn)換委托</param>
/// <returns>業(yè)務(wù)操作結(jié)果</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}個(gè)信息更新成功".FormatWith(dtos.Count))
: new OperationResult(OperationResultType.NoChanged);
}
如上高亮代碼,此方法定義了 Func<TEditDto, TEntity, Task> checkAction 和 Func<TEditDto, TEntity, Task<TEntity>> updateFunc 兩個(gè)委托參數(shù)作為 更新前參數(shù)檢查 和 更新后關(guān)聯(lián)更新 的操作傳入方式,方法中是以 OsharpException 類型異常來作為中止信號的,如果需要在委托中中止操作,直接拋 OsharpException 異常即可。在調(diào)用時(shí),即可極大簡化批量更新的操作,如上的更新代碼,簡化如下:
/// <summary>
/// 更新文章信息
/// </summary>
/// <param name="dtos">包含更新信息的文章信息DTO信息</param>
/// <returns>業(yè)務(wù)操作結(jié)果</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 的合法性,比如文章標(biāo)題要求唯一,dto.Title 就要驗(yàn)證唯一性
// 在這里要檢查 entity 的合法性,比如文章已鎖定,就不允許編輯,要進(jìn)行攔截
},
async (dto, entity) =>
{
// todo:
// 在這里要進(jìn)行其他實(shí)體的關(guān)聯(lián)更新,比如添加文章的編輯記錄
return entity;
});
}
如果沒有必要做額外的 更新前檢查 和更新后的 關(guān)聯(lián)更新,上面的批量更新可以簡化到極致:
/// <summary>
/// 更新文章信息
/// </summary>
/// <param name="dtos">包含更新信息的文章信息DTO信息</param>
/// <returns>業(yè)務(wù)操作結(jié)果</returns>
public virtual async Task<OperationResult> UpdatePosts(params PostInputDto[] dtos)
{
Check.Validate<PostInputDto, int>(dtos, nameof(dtos));
return await PostRepository.UpdateAsync(dtos);
}
服務(wù)層的事務(wù)管理
事務(wù)開啟與重用
OSharp的數(shù)據(jù)層在一次業(yè)務(wù)處理請求中遇到數(shù)據(jù)的 新增、更新、刪除 操作并第一次執(zhí)行 SaveChanges 操作時(shí),會自動開啟手動事務(wù),以后再次執(zhí)行 SaveChanges 操作時(shí),會直接使用 同一連接對象 的現(xiàn)有事務(wù),以保證一次業(yè)務(wù)請求的操作都自在一個(gè)事務(wù)內(nèi)。
public override int SaveChanges()
{
// ...
//開啟或使用現(xiàn)有事務(wù)
BeginOrUseTransaction();
int count = base.SaveChanges();
// ...
return count;
}
事務(wù)提交
為了方便事務(wù)管理和不同的服務(wù)層之間的事務(wù)同步,OSharp框架默認(rèn)的事務(wù)提交是在 API 層通過 MVC 的 UnitOfWorkAttribute 特性來提交的。
/// <summary>
/// 新用戶注冊
/// </summary>
/// <param name="dto">注冊信息</param>
/// <returns>JSON操作結(jié)果</returns>
[HttpPost]
[ServiceFilter(typeof(UnitOfWorkAttribute))]
[ModuleInfo]
[Description("用戶注冊")]
public async Task<AjaxResult> Register(RegisterDto dto)
{
// ...
}
當(dāng)然,你也可以不在 API 層標(biāo)注 [UnitOfWorkAttribute],而是在需要的時(shí)候通過 IUnitOfWork.Commit() 手動提交事務(wù)
IUnitOfWork unitOfWork = HttpContext.RequestServices.GetUnitOfWork<User, int>();
unitOfWork.Commit();
業(yè)務(wù)服務(wù)事件訂閱與發(fā)布
業(yè)務(wù)服務(wù)事件,是通過 事件總線EventBus 來實(shí)現(xiàn)的,OSharp構(gòu)建了一個(gè)簡單的事件總線基礎(chǔ)建設(shè),可以很方便地訂閱和發(fā)布業(yè)務(wù)事件。
訂閱事件
訂閱事件很簡單,只需要定義一組配套的 事件數(shù)據(jù)EventData 和相應(yīng)的 事件處理器EventHandler,即可完成事件訂閱的工作。
IEventData
事件數(shù)據(jù)EventData 是業(yè)務(wù)服務(wù)發(fā)布事件時(shí)向事件總線傳遞的數(shù)據(jù),每一種業(yè)務(wù),都有特定的事件數(shù)據(jù),一個(gè)事件數(shù)據(jù)可觸發(fā)多個(gè)事件處理器
定義一個(gè)事件數(shù)據(jù),需要實(shí)現(xiàn) IEventData 接口
/// <summary>
/// 定義事件數(shù)據(jù),所有事件都要實(shí)現(xiàn)該接口
/// </summary>
public interface IEventData
{
/// <summary>
/// 獲取 事件編號
/// </summary>
Guid Id { get; }
/// <summary>
/// 獲取 事件發(fā)生的時(shí)間
/// </summary>
DateTime EventTime { get; }
/// <summary>
/// 獲取或設(shè)置 事件源,觸發(fā)事件的對象
/// </summary>
object EventSource { get; set; }
}
EventDataBase
為了方便 事件數(shù)據(jù) 的定義,OSharp定義了一個(gè)通用事件數(shù)據(jù)基類EventDataBase,繼承此基類,只需要添加事件觸發(fā)需要的業(yè)務(wù)數(shù)據(jù)即可
/// <summary>
/// 事件源數(shù)據(jù)信息基類
/// </summary>
public abstract class EventDataBase : IEventData
{
/// <summary>
/// 初始化一個(gè)<see cref="EventDataBase"/>類型的新實(shí)例
/// </summary>
protected EventDataBase()
{
Id = Guid.NewGuid();
EventTime = DateTime.Now;
}
/// <summary>
/// 獲取 事件編號
/// </summary>
public Guid Id { get; }
/// <summary>
/// 獲取 事件發(fā)生時(shí)間
/// </summary>
public DateTime EventTime { get; }
/// <summary>
/// 獲取或設(shè)置 觸發(fā)事件的對象
/// </summary>
public object EventSource { get; set; }
}
IEventHandler
業(yè)務(wù)事件的處理邏輯,是通過 事件處理器 EventHandler 來實(shí)現(xiàn)的,事件處理器應(yīng)遵從 單一職責(zé) 原則,一個(gè)處理器只做一件事,業(yè)務(wù)服務(wù)層發(fā)布一項(xiàng) 事件數(shù)據(jù),可觸發(fā)多個(gè) 事件處理器
/// <summary>
/// 定義事件處理器,所有事件處理都要實(shí)現(xiàn)該接口
/// EventBus中,Handler的調(diào)用是同步執(zhí)行的,如果需要觸發(fā)就不管的異步執(zhí)行,可以在實(shí)現(xiàn)EventHandler的Handle邏輯時(shí)使用Task.Run
/// </summary>
[IgnoreDependency]
public interface IEventHandler
{
/// <summary>
/// 是否可處理指定事件
/// </summary>
/// <param name="eventData">事件源數(shù)據(jù)</param>
/// <returns>是否可處理</returns>
bool CanHandle(IEventData eventData);
/// <summary>
/// 事件處理
/// </summary>
/// <param name="eventData">事件源數(shù)據(jù)</param>
void Handle(IEventData eventData);
/// <summary>
/// 異步事件處理
/// </summary>
/// <param name="eventData">事件源數(shù)據(jù)</param>
/// <param name="cancelToken">異步取消標(biāo)識</param>
/// <returns></returns>
Task HandleAsync(IEventData eventData, CancellationToken cancelToken = default(CancellationToken));
}
泛型事件處理器
/// <summary>
/// 定義泛型事件處理器
/// EventBus中,Handler的調(diào)用是同步執(zhí)行的,如果需要觸發(fā)就不管的異步執(zhí)行,可以在實(shí)現(xiàn)EventHandler的Handle邏輯時(shí)使用Task.Run
/// </summary>
/// <typeparam name="TEventData">事件源數(shù)據(jù)</typeparam>
[IgnoreDependency]
public interface IEventHandler<in TEventData> : IEventHandler where TEventData : IEventData
{
/// <summary>
/// 事件處理
/// </summary>
/// <param name="eventData">事件源數(shù)據(jù)</param>
void Handle(TEventData eventData);
/// <summary>
/// 異步事件處理
/// </summary>
/// <param name="eventData">事件源數(shù)據(jù)</param>
/// <param name="cancelToken">異步取消標(biāo)識</param>
Task HandleAsync(TEventData eventData, CancellationToken cancelToken = default(CancellationToken));
}
EventHandlerBase
同樣的,為了方便 事件處理器 的定義,OSharp定義了一個(gè)通用的事件處理器基類EventHandlerBase<TEventData>,繼承此基類,只需要實(shí)現(xiàn)核心的事件處理邏輯即可
/// <summary>
/// 事件處理器基類
/// </summary>
public abstract class EventHandlerBase<TEventData> : IEventHandler<TEventData> where TEventData : IEventData
{
/// <summary>
/// 是否可處理指定事件
/// </summary>
/// <param name="eventData">事件源數(shù)據(jù)</param>
/// <returns>是否可處理</returns>
public virtual bool CanHandle(IEventData eventData)
{
return eventData.GetType() == typeof(TEventData);
}
/// <summary>
/// 事件處理
/// </summary>
/// <param name="eventData">事件源數(shù)據(jù)</param>
public virtual void Handle(IEventData eventData)
{
if (!CanHandle(eventData))
{
return;
}
Handle((TEventData)eventData);
}
/// <summary>
/// 異步事件處理
/// </summary>
/// <param name="eventData">事件源數(shù)據(jù)</param>
/// <param name="cancelToken">異步取消標(biāo)識</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">事件源數(shù)據(jù)</param>
public abstract void Handle(TEventData eventData);
/// <summary>
/// 異步事件處理
/// </summary>
/// <param name="eventData">事件源數(shù)據(jù)</param>
/// <param name="cancelToken">異步取消標(biāo)識</param>
/// <returns>是否成功</returns>
public virtual Task HandleAsync(TEventData eventData, CancellationToken cancelToken = default(CancellationToken))
{
return Task.Run(() => Handle(eventData), cancelToken);
}
}
發(fā)布事件
事件的發(fā)布,就相當(dāng)簡單了,只需要實(shí)例化一個(gè)事件數(shù)據(jù)EventData的實(shí)例,然后通過IEventBus.Publish(eventData)即可發(fā)布事件,觸發(fā)該EventData的所有訂閱處理器
XXXEventData eventData = new XXXEventData()
{
// ...
};
EventBus.Publish(eventData);
博客模塊的業(yè)務(wù)事件實(shí)現(xiàn)
回到我們的 Liuliu.Blogs 項(xiàng)目,根據(jù) <業(yè)務(wù)模塊設(shè)計(jì)#博客業(yè)務(wù)需求分析> 的需求分析的第二條,審核博客之后需要發(fā)郵件通知用戶,發(fā)郵件屬于審核博客業(yè)務(wù)計(jì)劃外的需求,使用 業(yè)務(wù)事件 來實(shí)現(xiàn)正當(dāng)其時(shí)。
- 審核博客業(yè)務(wù)事件數(shù)據(jù)
/// <summary>
/// 審核博客事件數(shù)據(jù)
/// </summary>
public class VerifyBlogEventData : EventDataBase
{
/// <summary>
/// 獲取或設(shè)置 博客名稱
/// </summary>
public string BlogName { get; set; }
/// <summary>
/// 獲取或設(shè)置 用戶名
/// </summary>
public string UserName { get; set; }
/// <summary>
/// 獲取或設(shè)置 審核是否通過
/// </summary>
public bool IsEnabled { get; set; }
}
- 審核博客業(yè)務(wù)事件處理器
/// <summary>
/// 審核博客事件處理器
/// </summary>
public class VerifyBlogEventHandler : EventHandlerBase<VerifyBlogEventData>
{
private readonly ILogger _logger;
/// <summary>
/// 初始化一個(gè)<see cref="VerifyBlogEventHandler"/>類型的新實(shí)例
/// </summary>
public VerifyBlogEventHandler(IServiceProvider serviceProvider)
{
_logger = serviceProvider.GetService<ILoggerFactory>().CreateLogger<VerifyBlogEventHandler>();
}
/// <summary>事件處理</summary>
/// <param name="eventData">事件源數(shù)據(jù)</param>
public override void Handle(VerifyBlogEventData eventData)
{
_logger.LogInformation(
$"觸發(fā) 審核博客事件處理器,用戶“{eventData.UserName}”的博客“{eventData.BlogName}”審核結(jié)果:{(eventData.IsEnabled ? "通過" : "未通過")}");
}
}
博客模塊的服務(wù)實(shí)現(xiàn)
回到我們的 Liuliu.Blogs 項(xiàng)目,根據(jù) <業(yè)務(wù)模塊設(shè)計(jì)#服務(wù)層> 的需求分析,綜合使用OSharp框架提供的基礎(chǔ)建設(shè),博客模塊的業(yè)務(wù)服務(wù)實(shí)現(xiàn)如下:
BlogsService.cs
/// <summary>
/// 業(yè)務(wù)服務(wù)實(shí)現(xiàn):博客模塊
/// </summary>
public partial class BlogsService : IBlogsContract
{
private readonly IServiceProvider _serviceProvider;
/// <summary>
/// 初始化一個(gè)<see cref="BlogsService"/>類型的新實(shí)例
/// </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>
/// 獲取 博客信息查詢數(shù)據(jù)集
/// </summary>
public virtual IQueryable<Blog> Blogs => BlogRepository.Query();
/// <summary>
/// 檢查博客信息是否存在
/// </summary>
/// <param name="predicate">檢查謂語表達(dá)式</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>業(yè)務(wù)操作結(jié)果</returns>
public virtual async Task<OperationResult> ApplyForBlog(BlogInputDto dto)
{
Check.Validate(dto, nameof(dto));
// 博客是以當(dāng)前用戶的身份來申請的
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, "當(dāng)前用戶已開通博客,不能重復(fù)申請");
}
if (await CheckBlogExists(m => m.Url == dto.Url))
{
return new OperationResult(OperationResultType.Error, $"Url 為“{dto.Url}”的博客已存在,不能重復(fù)添加");
}
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>業(yè)務(wù)操作結(jié)果</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)
{
// 查找博客主的角色,博主角色名可由配置系統(tǒng)獲得
const string roleName = "博主";
// 用于CUD操作的實(shí)體,要用 TrackQuery 方法來查詢出需要的數(shù)據(jù),不能用 Query,因?yàn)?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>業(yè)務(wù)操作結(jié)果</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}”的博客已存在,不能重復(fù)");
}
});
}
/// <summary>
/// 刪除博客信息
/// </summary>
/// <param name="ids">要?jiǎng)h除的博客信息編號</param>
/// <returns>業(yè)務(wù)操作結(jié)果</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>
/// 獲取 文章信息查詢數(shù)據(jù)集
/// </summary>
public virtual IQueryable<Post> Posts => PostRepository.Query();
/// <summary>
/// 檢查文章信息是否存在
/// </summary>
/// <param name="predicate">檢查謂語表達(dá)式</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>業(yè)務(wù)操作結(jié)果</returns>
public virtual async Task<OperationResult> CreatePosts(params PostInputDto[] dtos)
{
Check.Validate<PostInputDto, int>(dtos, nameof(dtos));
if (dtos.Length == 0)
{
return OperationResult.NoChanged;
}
// 文章是以當(dāng)前用戶身份來添加的
ClaimsPrincipal principal = _serviceProvider.GetCurrentUser();
if (principal == null || !principal.Identity.IsAuthenticated)
{
throw new OsharpException("用戶未登錄或登錄已失效");
}
// 檢查當(dāng)前用戶的博客狀態(tài)
int userId = principal.Identity.GetUserId<int>();
Blog blog = BlogRepository.TrackQuery(m => m.UserId == userId).FirstOrDefault();
if (blog == null || !blog.IsEnabled)
{
throw new OsharpException("當(dāng)前用戶的博客未開通,無法添加文章");
}
// 沒有前置檢查,checkAction為null
return await PostRepository.InsertAsync(dtos, null, (dto, entity) =>
{
// 給新建的文章關(guān)聯(lián)博客和作者
entity.BlogId = blog.Id;
entity.UserId = userId;
return Task.FromResult(entity);
});
}
/// <summary>
/// 更新文章信息
/// </summary>
/// <param name="dtos">包含更新信息的文章信息DTO信息</param>
/// <returns>業(yè)務(wù)操作結(jié)果</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">要?jiǎng)h除的文章信息編號</param>
/// <returns>業(yè)務(wù)操作結(jié)果</returns>
public virtual Task<OperationResult> DeletePosts(params int[] ids)
{
Check.NotNull(ids, nameof(ids));
return PostRepository.DeleteAsync(ids);
}
}
模塊入口 BlogsPack
模塊入口基類
非AspNetCore模塊基類 OsharpPack
前面多次提到,每個(gè)Pack模塊都是繼承自一個(gè) 模塊基類OsharpPack,這個(gè)基類用于定義 模塊初始化UsePack 過程中未涉及 AspNetCore 環(huán)境的模塊。
/// <summary>
/// OSharp模塊基類
/// </summary>
public abstract class OsharpPack
{
/// <summary>
/// 獲取 模塊級別,級別越小越先啟動
/// </summary>
public virtual PackLevel Level => PackLevel.Business;
/// <summary>
/// 獲取 模塊啟動順序,模塊啟動的順序先按級別啟動,同一級別內(nèi)部再按此順序啟動,
/// 級別默認(rèn)為0,表示無依賴,需要在同級別有依賴順序的時(shí)候,再重寫為>0的順序值
/// </summary>
public virtual int Order => 0;
/// <summary>
/// 獲取 是否已可用
/// </summary>
public bool IsEnabled { get; protected set; }
/// <summary>
/// 將模塊服務(wù)添加到依賴注入服務(wù)容器中
/// </summary>
/// <param name="services">依賴注入服務(wù)容器</param>
/// <returns></returns>
public virtual IServiceCollection AddServices(IServiceCollection services)
{
return services;
}
/// <summary>
/// 應(yīng)用模塊服務(wù)
/// </summary>
/// <param name="provider">服務(wù)提供者</param>
public virtual void UsePack(IServiceProvider provider)
{
IsEnabled = true;
}
/// <summary>
/// 獲取當(dāng)前模塊的依賴模塊類型
/// </summary>
/// <returns></returns>
internal Type[] GetDependPackTypes(Type packType = null)
{
// ...
}
}
模塊基類OsharpPack 定義了兩個(gè)可重寫屬性:
PackLevel:模塊級別,級別越小越先啟動
模塊級別按 模塊 在框架中不同的功能層次,定義了如下幾個(gè)級別:
/// <summary>
/// 模塊級別,級別越核心,優(yōu)先啟動
/// </summary>
public enum PackLevel
{
/// <summary>
/// 核心級別,表示系統(tǒng)的核心模塊,
/// 這些模塊不涉及第三方組件,在系統(tǒng)運(yùn)行中是不可替換的,核心模塊將始終加載
/// </summary>
Core = 1,
/// <summary>
/// 框架級別,表示涉及第三方組件的基礎(chǔ)模塊
/// </summary>
Framework = 10,
/// <summary>
/// 應(yīng)用級別,表示涉及應(yīng)用數(shù)據(jù)的基礎(chǔ)模塊
/// </summary>
Application = 20,
/// <summary>
/// 業(yè)務(wù)級別,表示涉及真實(shí)業(yè)務(wù)處理的模塊
/// </summary>
Business = 30
}
Order:級別內(nèi)模塊啟動順序,模塊啟動的順序先按級別啟動,同一級別內(nèi)部再按此順序啟動,級別默認(rèn)為 0,表示無依賴,需要在同級別有依賴順序的時(shí)候,再重寫為 >0 的順序值
同時(shí),模塊基類 還定義了兩個(gè)方法:
AddServices:用于將模塊內(nèi)定義的服務(wù)注入到 依賴注入服務(wù)容器 中。UsePack:用于使用服務(wù)對當(dāng)前模塊進(jìn)行初始化。
AspNetCore模塊基類 AspOsharpPack
AspOsharpPack 基類繼承了 OsharpPack,添加了一個(gè)對 IApplicationBuilder 支持的 UsePack 方法,用于實(shí)現(xiàn)與 AspNetCore 關(guān)聯(lián)的模塊初始化工作,例如 Mvc模塊 初始化的時(shí)候需要應(yīng)用中間件:app.UseMvcWithAreaRoute();
/// <summary>
/// 基于AspNetCore環(huán)境的Pack模塊基類
/// </summary>
public abstract class AspOsharpPack : OsharpPack
{
/// <summary>
/// 應(yīng)用AspNetCore的服務(wù)業(yè)務(wù)
/// </summary>
/// <param name="app">Asp應(yīng)用程序構(gòu)建器</param>
public virtual void UsePack(IApplicationBuilder app)
{
base.UsePack(app.ApplicationServices);
}
}
博客模塊的模塊入口實(shí)現(xiàn)
回到我們的 Liuliu.Blogs 項(xiàng)目,我們來實(shí)現(xiàn)投票模塊的模塊入口類 BlogsPack:
- 博客模塊屬于業(yè)務(wù)模塊,因此
PackLevel設(shè)置為Business - 博客模塊的啟動順序無需重寫,保持 0 即可
- 將 博客業(yè)務(wù)服務(wù) 注冊到 服務(wù)容器中
- 無甚初始化業(yè)務(wù)
實(shí)現(xiàn)代碼如下:
/// <summary>
/// 博客模塊
/// </summary>
public class BlogsPack : OsharpPack
{
/// <summary>
/// 獲取 模塊級別,級別越小越先啟動
/// </summary>
public override PackLevel Level { get; } = PackLevel.Business;
/// <summary>將模塊服務(wù)添加到依賴注入服務(wù)容器中</summary>
/// <param name="services">依賴注入服務(wù)容器</param>
/// <returns></returns>
public override IServiceCollection AddServices(IServiceCollection services)
{
services.TryAddScoped<IBlogsContract, BlogsService>();
return services;
}
}
至此,博客模塊的服務(wù)層實(shí)現(xiàn)完畢。
作者:郭明鋒
Q群:MVC EF技術(shù)交流(5008599)
OSharp開發(fā)框架交流(85895249)
出處:http://www.rzrgm.cn/guomingfeng
聲明:本文版權(quán)歸作者和博客園共有,歡迎轉(zhuǎn)載,但未經(jīng)作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責(zé)任的權(quán)利。


一個(gè)業(yè)務(wù)模塊,是負(fù)責(zé)完成一系列功能的,這些功能相互之間具有密切的關(guān)聯(lián)性,所以對于一個(gè)模塊來說,業(yè)務(wù)服務(wù)是一個(gè)整體,不應(yīng)把他們再按單個(gè)實(shí)體拆分開來。OSharp 的業(yè)務(wù)模塊代碼結(jié)構(gòu)設(shè)計(jì),也是根據(jù)這一原則來設(shè)計(jì)的。設(shè)計(jì)規(guī)則如下
浙公網(wǎng)安備 33010602011771號