应用服务(Aegis.Services)
Aegis.Services 负责业务服务契约、服务自动注入、分页结果和服务结果约定。它对应 Component.deps.json 里的 BusinessServices 组件项,是 Aegis 业务层最核心的基础组件之一。
组件概览
| 字段 | 说明 |
|---|---|
| 组件名称 | 应用服务 |
| 真实类库 | Aegis.Services |
| 组件定位 | 业务服务契约与服务实现的自动装配组件 |
| 引入方式 | 安装 NuGet,并在 Component.deps.json 的 Services 中启用 BusinessServices |
| 组件声明 | BusinessServices |
| 核心能力 | 自动注册服务、契约约定、Mapster 映射注册、PagedResult、ServiceResult |
| 典型配套 | Aegis.Core.Infrastructure、Repository 层、Aegis.Configuration |
什么时候要用它
适合场景:
- 你要定义领域服务契约并注入到控制器
- 你希望
.Contract和.Services按约定自动装配 - 你要在服务层统一使用
PagedResult、ServiceResult - 你希望 DTO 映射注册跟随服务程序集自动扫描
最小可运行路径
第一步:在组件配置里启用 BusinessServices
{
"Components": {
"Services": [
"BusinessServices"
],
"Middlewares": []
}
}
第二步:定义契约接口
服务契约接口需要继承 IBusinessService。
using Aegis.Services;
public interface IUserContract : IBusinessService
{
Task<bool> ChangeUser(int id);
Task<bool> ChangeUserStatus(int id, int statusCode);
}
第三步:实现服务
public class UserRepository : BaseRepository<UserEntity, int>
{
public UserRepository(IFreeSql<AegisDb> fsql) : base(fsql)
{
}
public Task<UserEntity> GetByIdAsync(int id)
{
return this.Where(x => x.Id == id).FirstAsync();
}
}
public class UserService : IUserContract
{
private readonly UserRepository _userRepository;
public UserService(UserRepository userRepository)
{
_userRepository = userRepository;
}
public async Task<bool> ChangeUser(int id)
{
var user = await _userRepository.GetByIdAsync(id);
user.IsDeleted = true;
return await _userRepository.UpdateAsync(user);
}
public Task<bool> ChangeUserStatus(int id, int statusCode)
{
return Task.FromResult(true);
}
}
第四步:在控制器里直接注入契约
public class UserController : ApiControllerBase
{
private readonly IUserContract _userService;
public UserController(IUserContract userService)
{
_userService = userService;
}
}
自动注入是怎么生效的
启用 BusinessServices 后,Aegis 会扫描运行目录下符合规则的程序集:
*.Contract.dll*.Services.dll
然后按下面规则自动注册:
- 找到继承
IBusinessService的接口,按接口找实现类并注册 - 找到继承
IBusinessService的具体类,也会注册自身 - 同时扫描
IRegister,自动加载 Mapster 映射配置
这也是为什么服务层命名和工程结构最好保持规范。
工程结构建议怎么落
推荐保持这组结构:
XXX.ContractXXX.DtoXXX.Services
例如:
His.Invoice.Contract
His.Invoice.Dto
His.Invoice.Services
这样做的好处是:
- 契约层和实现层边界清楚
- 自动扫描规则更稳定
- 控制器、服务、DTO 的职责更容易长期维护
如果你们准备把 Services 层作为 DDD 起点层,建议在 XXX.Services 项目内部继续按领域拆文件夹,并在文件夹内区分应用服务和领域服务。更具体的判断方式见 Services与Contract。
Contract 和 Services 该怎么分
Contract 层负责什么
Contract 层只负责定义服务接口,不承载业务实现。
public interface IAccessPartyService : IBusinessService
{
Task<PagedResult<GetAccessPartyDto>> GetAccessPartyListByPage(
GetAccessPartyListRequest request);
}
Services 层负责什么
Services 层负责业务聚合、规则处理、调用仓储和其他组件,不负责定义控制器请求模型,也原则上不直接访问 FreeSql 或其他 ORM。
public class AccessPartyService : IAccessPartyService
{
public Task<PagedResult<GetAccessPartyDto>> GetAccessPartyListByPage(
GetAccessPartyListRequest request)
{
throw new NotImplementedException();
}
}
使用时建议守住这条边界:
- Controller 收请求
- Services 做业务
- Repository 做数据访问
- Dto 做跨层传输
尤其建议明确这一条:
- Services 通过 Repository 获取数据库信息,不直接在服务里操作
FreeSql - Aegis 当前更常见的是直接注入具体仓储类,而不是额外定义仓储接口
分页结果怎么返回
服务层分页统一使用 PagedResult<T>。
public readonly record struct PagedResult<T>(
List<T> Entities,
long Total,
int CurrentIndex,
int PageSize);
如果仓储层返回的是 PagedList<T>,可以直接转成服务层结果:
var list = _ipVisitRepository.GetRyIpVisitList(filter);
return DtoPaged<GetRyIpVisitListDto>.Convert(list);
适合场景:
- 需要返回分页数据给控制器
- 需要把仓储层结果转成 DTO 分页结果
ServiceResult 什么时候该用
默认优先直接返回业务结果,例如:
boolintDtoList<T>PagedResult<T>
只有当业务存在明确的多分支结果时,再考虑 ServiceResult<T>。
Success / Failed / Error 怎么区分
| 方法 | 适用场景 | 行为 |
|---|---|---|
ServiceResult.Success(...) | 业务成功,且你希望返回统一结果结构 | 返回 IsSuccess = true 的结果对象 |
ServiceResult.Failed(...) | 业务失败,但这仍然属于可预期业务分支 | 返回 IsSuccess = false 的结果对象,不抛异常 |
ServiceResult.Error(...) | 发生异常,且你希望中断当前请求并走异常链路 | 直接抛出 BusinessException,不返回普通结果对象 |
这三者可以这样理解:
Success:正常完成Failed:业务没通过,但仍然是“可预期失败”Error:已经进入异常处理语义
Success 用法
适合场景:
- 查询成功并返回 DTO
- 创建成功并返回主键、布尔值或对象
- 希望返回统一消息和消息类型
public async Task<ServiceResult<UserDto>> GetUserAsync(int id)
{
var user = await _userRepository.GetDtoAsync(id);
return ServiceResult.Success(
MessageType.Ignore,
"获取用户成功",
user);
}
如果你只想给出默认成功结果,也可以直接传数据:
return ServiceResult.Success(user);
Failed 用法
适合场景:
- 参数校验未通过
- 业务前置条件不满足
- 数据不存在,但不应该按异常处理
- 重复提交、状态不允许、库存不足这类业务失败
public async Task<ServiceResult<VisitPayTypeDto>> GetVisitPayTypeBySeq(long visitPayTypeSeq)
{
if (visitPayTypeSeq <= 0)
{
return ServiceResult.Failed<VisitPayTypeDto>(
MessageType.Notice,
"获取身份信息失败,入参错误");
}
var data = await _visitPayTypeRepository
.Where(o => !o.IsDeleted && o.VisitPayTypeSeq == visitPayTypeSeq)
.FirstAsync();
if (data == null)
{
return ServiceResult.Failed<VisitPayTypeDto>(
MessageType.Notice,
"获取身份信息失败,未能查询到信息");
}
return ServiceResult.Success(
MessageType.Ignore,
"获取身份信息成功",
data.Adapt<VisitPayTypeDto>());
}
如果你只想返回简单失败消息,也可以用简写:
return ServiceResult.Failed("保存失败");
Error 用法
适合场景:
- 捕获到了真实异常
- 需要保留异常栈
- 需要交给框架统一异常处理与日志记录
- 当前请求不应该继续往下执行
public async Task<ServiceResult<InvoiceDto>> IssueInvoiceAsync(IssueInvoiceRequest request)
{
try
{
var invoice = await _invoiceRepository.CreateAsync(request);
return ServiceResult.Success(invoice);
}
catch (Exception ex)
{
return ServiceResult.Error<InvoiceDto>(ex, "开票异常");
}
}
如果你还想附带额外错误上下文,可以使用带 data 的重载:
catch (Exception ex)
{
return ServiceResult.Error<InvoiceDto>(
ex,
"开票异常",
new { request.InvoiceCode, request.PatientId });
}
注意:
Error(...)本质上会抛出BusinessException- 它不会像
Failed(...)那样返回一个普通失败对象 - 如果你不想中断请求,就不要用
Error(...)
常规场景
public async Task<bool> CreateAccessParty(CreateAccessPartyRequest request)
{
var accessParty = request.Adapt<AccessPartyEntity>();
return await _accessPartyRepository.CreateAsync(accessParty);
}
复杂分支场景
public async Task<ServiceResult<VisitPayTypeDto>> GetVisitPayTypeBySeq(long visitPayTypeSeq)
{
if (visitPayTypeSeq <= 0)
{
return ServiceResult.Failed<VisitPayTypeDto>(
MessageType.Notice,
"获取身份信息失败,入参错误");
}
var data = await _visitPayTypeRepository
.Where(o => !o.IsDeleted && o.VisitPayTypeSeq == visitPayTypeSeq)
.FirstAsync();
if (data == null)
{
return ServiceResult.Failed<VisitPayTypeDto>(
MessageType.Notice,
"获取身份信息失败,未能查询到信息");
}
return ServiceResult.Success(
MessageType.Ignore,
"获取身份信息成功",
data.Adapt<VisitPayTypeDto>());
}
异常场景
ServiceResult.Error(...) 会直接抛出业务异常,而不是静默返回失败对象。
try
{
// 业务代码
}
catch (Exception ex)
{
return ServiceResult.Error<MyResponse>(ex, "处理失败");
}
如果你不想中断当前请求,而只是返回业务失败,请记录日志后用 ServiceResult.Failed(...)。
接入完成后怎么确认成功
你至少应该确认这些点:
Component.deps.json的Services中已经包含BusinessServices- Contract 接口继承了
IBusinessService - 控制器可以直接注入契约接口
.Contract与.Services程序集能被自动扫描到- Services 通过 Repository 访问数据库,而不是直接依赖
FreeSql PagedResult、ServiceResult的使用边界清楚
常见问题
为什么控制器注入服务契约时报 DI 错误
最常见的排查顺序是:
Component.deps.json里是否启用了BusinessServices- 契约接口是否继承了
IBusinessService - 服务类是否真正实现了对应契约
- 启动项目是否引用了对应的
XXX.Services项目 XXX.Contract.dll和XXX.Services.dll是否已经进入运行目录
最容易漏的是第 4 点。BusinessServices 不是扫描整个源码目录,而是扫描运行目录里的 *.Contract.dll 和 *.Services.dll。如果启动项目没有引用对应服务项目,最终就会表现成“接口存在,但 DI 找不到实现”。
为什么这里不再写仓储接口
当前 Aegis 常见写法里,Repository 层一般是 FreeSql 的具体仓储类:
public class UserRepository : BaseRepository<UserEntity, int>
{
public UserRepository(IFreeSql<AegisDb> fsql) : base(fsql)
{
}
}
public class UserService : IUserContract
{
private readonly UserRepository _userRepository;
public UserService(UserRepository userRepository)
{
_userRepository = userRepository;
}
}
也就是说,当前主流用法是服务层直接注入具体仓储类,而不是再包一层仓储接口。
为什么明明开了 BusinessServices,服务还是没注册
除了组件配置,还要继续检查:
- 服务项目命名是否符合
*.Services - 契约项目命名是否符合
*.Contract - 服务实现类是否是可实例化的具体类
- 服务项目是否真的被启动项目引用
Services 层为什么不建议直接注入 IFreeSql
因为当前分层里:
- Repository 负责数据访问
- Services 负责业务聚合
如果服务层直接操作 IFreeSql,很容易把查询细节、ORM 使用方式和业务规则揉在一起,后期维护成本会明显上升。当前文档统一建议把数据库访问收敛到具体仓储类里。