[開源]OSharpNS 步步為營系列 - 3. 添加業務服務層

来源:https://www.cnblogs.com/guomingfeng/archive/2019/05/11/osharpns-steps-service.html
-Advertisement-
Play Games

一個業務模塊,是負責完成一系列功能的,這些功能相互之間具有密切的關聯性,所以對於一個模塊來說,業務服務是一個整體,不應把他們再按單個實體拆分開來。OSharp 的業務模塊代碼結構設計,也是根據這一原則來設計的。設計規則如下 ...


什麼是OSharp

OSharpNS全稱OSharp Framework with .NetStandard2.0,是一個基於.NetStandard2.0開發的一個.NetCore快速開發框架。這個框架使用最新穩定版的.NetCore SDK(當前是.NET Core 2.2),對 AspNetCore 的配置、依賴註入、日誌、緩存、實體框架、Mvc(WebApi)、身份認證、許可權授權等模塊進行更高一級的自動化封裝,並規範了一套業務實現的代碼結構與操作流程,使 .Net Core 框架更易於應用到實際項目開發中。

概述

一個模塊的服務層,主要負責如下幾個方面的工作:

  • 向 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,例如文章的更新操作,以下幾個步驟是免不了的:

  1. dto.Id 查找出相應的文章實體 entity,如果不存在,中止操作並返回
  2. 進行更新前的數據檢查
    1. 檢查 dto 的合法性,比如文章標題要求唯一,dto.Title 就要驗證唯一性,中止操作並返回
    2. 檢查 entity 的合法性,比如文章已鎖定,就不允許編輯,要進行攔截,檢查不通過,中止操作並返回
  3. 使用 AutoMapperdto 的值更新到 entity
  4. 進行其他關聯實體的更新
    1. 比如添加文章的編輯記錄
    2. 比如給當前操作人加積分
  5. 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> checkActionFunc<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;
    }
}

至此,博客模塊的服務層實現完畢。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在實際開發中經常遇到時間格式的轉換,例如: 前端傳遞的時間格式是字元串格式,我們需要將其轉換為時間戳,或者前臺傳遞的時間格式和我們資料庫中的格式不對應,我們需要對其進行轉換才能與資料庫的時間進行匹配等。 1、將字元串時間轉換成時間戳 將字元串時間轉換成時間組後在將其轉換成時間戳格式 得到時間組對象後 ...
  • 題目鏈接 Description A power network consists of nodes (power stations, consumers and dispatchers) connected by power transport lines. A node u may be sup ...
  • 寫了一個練手的爬蟲...在輸出的時候出現了讓人很不愉♂悅的問題 像這樣: 寫了一個練手的爬蟲...在輸出的時候出現了讓人很不愉♂悅的問題 像這樣: 令人十分難受啊! # 令人十分難受啊! # 在此之前先說一下python中的.format格式化輸出 python2.6開始,可以使用str.forma ...
  • JAVA語言 Java分為三個體系: •JavaSE(J2SE)(Java 2 Platform Standard Edition,java平臺標準版)主要用於桌面級的應用和資料庫的開發,包含構成Java語言核心的類, 如資料庫連接、 介面定義、輸入/輪出和網路編程。 •JavaEE(J2EE)(J ...
  • if判斷 我們人有判斷的功能,電腦既然模仿人,那就也一定有判斷的功能。 Python中的判斷使用 “if” 判斷語法 if判斷是乾什麼的呢?if判斷其實是在模擬人做判斷。就是說如果這樣乾什麼,如果那樣乾什麼 if 條件: 代碼塊 代碼1 代碼2 代碼3 ... 代表快(同一縮進級別的代碼,列如代碼 ...
  • 一 什麼是消息隊列 我們可以把消息隊列比作是一個存放消息的容器,當我們需要使用消息的時候可以取出消息供自己使用。消息隊列是分散式系統中重要的組件,使用消息隊列主要是為了通過非同步處理提高系統性能和削峰、降低系統耦合性。目前使用較多的消息隊列有ActiveMQ,RabbitMQ,Kafka,Rocket ...
  • 基於flask的網頁聊天室(二) 前言 接上一次的內容繼續完善,今天完成的內容不是很多,只是簡單的用戶註冊登錄,內容具體如下 具體內容 這次要加入與數據哭交互的操作,所以首先要建立相關表結構,這裡使用flask-sqlalchemy來輔助創建 首先修改之前的init文件為: from flask i ...
  • 5.10自我總結 1.編碼解碼 1.字元串編碼 字元 》翻譯過程 》數字 2.字元串解碼 字元 》翻譯過程 》數字 3.編碼解碼用到的翻譯工具 中國:GBK 外國:日本—Shift_JIS,美國ASCII,南韓Euc kr 國際統一:Unicode進行編寫,存取用UTF 8,Unicode與UTF ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...