跳到主要内容
版本:3.0.0

应用服务(Aegis.Services)

Aegis.Services 负责业务服务契约、服务自动注入、分页结果和服务结果约定。它对应 Component.deps.json 里的 BusinessServices 组件项,是 Aegis 业务层最核心的基础组件之一。

组件概览

字段说明
组件名称应用服务
真实类库Aegis.Services
组件定位业务服务契约与服务实现的自动装配组件
引入方式安装 NuGet,并在 Component.deps.jsonServices 中启用 BusinessServices
组件声明BusinessServices
核心能力自动注册服务、契约约定、Mapster 映射注册、PagedResultServiceResult
典型配套Aegis.Core.InfrastructureRepository 层、Aegis.Configuration

什么时候要用它

适合场景:

  • 你要定义领域服务契约并注入到控制器
  • 你希望 .Contract.Services 按约定自动装配
  • 你要在服务层统一使用 PagedResultServiceResult
  • 你希望 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.Contract
  • XXX.Dto
  • XXX.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 什么时候该用

默认优先直接返回业务结果,例如:

  • bool
  • int
  • Dto
  • List<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.jsonServices 中已经包含 BusinessServices
  • Contract 接口继承了 IBusinessService
  • 控制器可以直接注入契约接口
  • .Contract.Services 程序集能被自动扫描到
  • Services 通过 Repository 访问数据库,而不是直接依赖 FreeSql
  • PagedResultServiceResult 的使用边界清楚

常见问题

为什么控制器注入服务契约时报 DI 错误

最常见的排查顺序是:

  1. Component.deps.json 里是否启用了 BusinessServices
  2. 契约接口是否继承了 IBusinessService
  3. 服务类是否真正实现了对应契约
  4. 启动项目是否引用了对应的 XXX.Services 项目
  5. XXX.Contract.dllXXX.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 使用方式和业务规则揉在一起,后期维护成本会明显上升。当前文档统一建议把数据库访问收敛到具体仓储类里。

下一步看哪里