什么是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;
}
}

至此,博客模块的服务层实现完毕。

[开源]OSharpNS 步步为营系列 - 3. 添加业务服务层的更多相关文章

  1. [开源]OSharpNS 步步为营系列 - 4. 添加业务对外API

    什么是OSharp OSharpNS全称OSharp Framework with .NetStandard2.0,是一个基于.NetStandard2.0开发的一个.NetCore快速开发框架.这个 ...

  2. [开源]OSharpNS 步步为营系列 - 2. 添加业务数据层

    什么是OSharp OSharpNS全称OSharp Framework with .NetStandard2.0,是一个基于.NetStandard2.0开发的一个.NetCore快速开发框架.这个 ...

  3. [开源]OSharpNS 步步为营系列 - 5. 添加前端Angular模块[完结]

    什么是OSharp OSharpNS全称OSharp Framework with .NetStandard2.0,是一个基于.NetStandard2.0开发的一个.NetCore快速开发框架.这个 ...

  4. [开源]OSharpNS 步步为营系列 - 1. 业务模块设计

    什么是OSharp OSharpNS全称OSharp Framework with .NetStandard2.0,是一个基于.NetStandard2.0开发的一个.NetCore快速开发框架.这个 ...

  5. Map工具系列-05-添加业务参数工具

    所有cs端工具集成了一个工具面板 -打开(IE) Map工具系列-01-Map代码生成工具说明 Map工具系列-02-数据迁移工具使用说明 Map工具系列-03-代码生成BySQl工具使用说明 Map ...

  6. 开源分布式Job系统,调度与业务分离-如何创建一个计划HttpJob任务

    项目介绍: Hangfire:是一个开源的job调度系统,支持分布式JOB!! Hangfire.HttpJob 是我针对Hangfire开发的一个组件,该组件和Hangfire本身是独立的.可以独立 ...

  7. 开源分布式Job系统,调度与业务分离-如何创建周期性的HttpJob任务

    项目介绍: Hangfire:是一个开源的job调度系统,支持分布式JOB!! Hangfire.HttpJob 是我针对Hangfire开发的一个组件,该组件和Hangfire本身是独立的.可以独立 ...

  8. 开源分布式Job系统,调度与业务分离-HttpJob.Agent组件介绍以及如何使用

    项目介绍: Hangfire:是一个开源的job调度系统,支持分布式JOB!! Hangfire.HttpJob 是我针对Hangfire开发的一个组件,该组件和Hangfire本身是独立的.可以独立 ...

  9. Quartz.NET开源作业调度框架系列

    Quartz.NET是一个被广泛使用的开源作业调度框架 , 由于是用C#语言创建,可方便的用于winform和asp.net应用程序中.Quartz.NET提供了巨大的灵活性但又兼具简单性.开发人员可 ...

随机推荐

  1. ASP.NET CORE系列【六】Entity Framework Core 之数据迁移

    原文:ASP.NET CORE系列[六]Entity Framework Core 之数据迁移 前言 最近打算用.NET Core写一份简单的后台系统,来练练手 然后又用到了Entity Framew ...

  2. 【Ubuntu】查看进程端口占用信息

    1.查看进程信息 ①进程查看 ps -aux ②根据PID查看进程 ps -aux | grep pid ③进程计数,查看指定进程 ps -aux | wc -l ps -aux | grep kwo ...

  3. Win8 Metro(C#)数字图像处理--2.51图像统计滤波算法

    原文:Win8 Metro(C#)数字图像处理--2.51图像统计滤波算法  [函数名称]   图像统计滤波   WriteableBitmap StatisticalFilter(Writeab ...

  4. linux安装脚本

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 3 ...

  5. Android零基础入门第12节:熟悉Android Studio界面,开始装逼卖萌

    原文:Android零基础入门第12节:熟悉Android Studio界面,开始装逼卖萌 通过前两期的学习,我们可以正确搭建好Android Studio的开发环境,也创建了HelloWorld工程 ...

  6. Unicode对象

    什么是Unicode对象呢? 你可以认为unicode对象就是一个Python字符串,它可以处理上百万不同类别的字符——从古老版本的Latin字符到非Latin字符,再到曲折的引用和艰涩的符号. 普通 ...

  7. WPF 用Main函数方式启动程序

    原文:WPF 用Main函数方式启动程序 WPF默认程序启动:新建project后自动生成的App.xaml中指定程序启动方式(StartupUri="MainWindow.xaml&quo ...

  8. win10应用开发——如何判断应用是在手机上运行还是电脑上运行

    原文:win10应用开发--如何判断应用是在手机上运行还是电脑上运行 在进行uwp应用开发的时候, 有时我们需要知道自己的应用是在手机端运行还是在桌面端运行,那么通过以下的api就可以进行判断: Wi ...

  9. MySQL 其它基本操作

    索引 所谓索引,就是类似于书的目录,目的也类似,都是为了提高检索速度.ALTER TABLE <表名> ADD INDEX <索引名(列名)>;或者CREATE INDEX & ...

  10. Delphi获得一个进程的主窗体(GetWindow(AHandle, GW_OWNER)等于0的窗体才是主窗体,并且要IsWindowVisible排除Application窗口)

    type  TMainWindow = packed record    ProcessID: THandle;    MainWindow: THandle;  end;  PMainWindow  ...