如何规范组织UserService:DTO、Include及异常处理?
技术栈
使用Mapster和Serilog
代码实现
public class UserService(IUserRepository repository, ILogger logger) { public async Task<TDto?> GetByIdAsync<TDto>(int userId, Func<IQueryable<User>, IQueryable<User>>? include = null, CancellationToken cancellationToken = default) { try { var entity = await repository.GetByIdAsync(userId, include, cancellationToken); return entity is null ? default : entity.Adapt<TDto?>(); } catch (OracleException ex) when (ex.Number == 1013) { logger.Information("The request was cancelled."); return default; } } public async Task<IEnumerable<TDto>> GetByNameAsync<TDto>(string name, CancellationToken cancellationToken = default) { try { var entities = await repository.GetByNameAlnumAsync(name, cancellationToken); return entities.Adapt<IEnumerable<TDto>>(); } catch (OracleException ex) when (ex.Number == 1013) { logger.Information("The request was cancelled."); return []; } } public async Task<IEnumerable<TDto>> GetByFilterAsync<TDto>(Filter filter, CancellationToken cancellationToken = default) { try { var entities = await repository.GetByFilterAsync(filter, cancellationToken); return entities.Adapt<IEnumerable<TDto>>(); } catch (OracleException ex) when (ex.Number == 1013) { logger.Information("The request was cancelled."); return []; } } }
技术问询
- 我当前对TDto的使用方式是否正确?
- GetByIdAsync方法中的include参数违反了单一职责原则(SRP)。假设User存在大量外部依赖,且各处使用方式不同,是否需要针对每种Include组合编写独立方法?
- 如何合理组织try catch块?是否应采用Result对象模式?如何规避代码重复?是否不应在该服务中编写异常处理方法,而是使用专门的异常处理服务?需要传递哪些相关信息?
问题1:TDto的使用方式是否正确?
你的TDto用法是合理且符合常规实践的。通过泛型参数让同一个查询方法适配不同的DTO输出,避免了为每种DTO重复编写几乎一样的查询逻辑,这正是泛型的优势所在。
不过有几个小建议:
- 可以给
TDto加上class约束(where TDto : class),因为Adapt<TDto?>针对引用类型更明确,避免值类型的不必要空处理; - 如果你的DTO有固定的映射规则,确保Mapster已经正确配置了
User到目标DTO的映射,避免运行时出现映射失败的问题。
问题2:include参数是否违反SRP?是否需要为每种Include组合写独立方法?
首先,include参数本身不算违反SRP——它的核心是允许调用方灵活控制数据加载范围,而不是让UserService承担数据加载策略的职责。如果为每种Include组合写独立方法,很快会导致方法爆炸(比如GetUserWithOrdersAsync、GetUserWithProfileAndRolesAsync等),维护成本极高。
替代方案:
- 使用规范查询对象(Specification Pattern):把Include逻辑封装到独立的Specification类中,比如
UserWithOrdersSpec、UserWithFullDependenciesSpec,然后让GetByIdAsync接收ISpecification<User>参数,这样既保持了灵活性,又把查询逻辑和服务逻辑解耦; - 限定Include的范围:如果担心调用方滥用Include导致性能问题,可以在Repository层对Include的导航属性做白名单校验,只允许加载预定义的合法依赖;
- 按需暴露专用方法:对于某些高频使用的Include组合,可以单独写方法(比如
GetUserWithBasicProfileAsync),但不要覆盖所有场景,只处理最常用的情况。
问题3:如何合理组织try catch块?是否用Result模式?如何避免重复?
你当前的代码存在明显的异常处理重复——三个方法都捕获相同的OracleException 1013并做相同的处理。优化方向如下:
1. 提取通用异常处理逻辑
把重复的catch逻辑封装成通用方法,比如:
private async Task<TResult> HandleCancellationExceptionAsync<TResult>(Func<Task<TResult>> operation) { try { return await operation(); } catch (OracleException ex) when (ex.Number == 1013) { logger.Information("请求已取消"); return default!; } }
然后在业务方法中调用:
public async Task<TDto?> GetByIdAsync<TDto>(int userId, Func<IQueryable<User>, IQueryable<User>>? include = null, CancellationToken cancellationToken = default) { return await HandleCancellationExceptionAsync(async () => { var entity = await repository.GetByIdAsync(userId, include, cancellationToken); return entity?.Adapt<TDto?>(); }); }
2. 是否使用Result对象模式?
如果你的业务场景需要返回更丰富的操作结果信息(比如错误码、错误消息,而不仅仅是默认值),Result模式是值得考虑的。比如定义一个通用的Result<T>:
public class Result<T> { public bool IsSuccess { get; set; } public T? Data { get; set; } public string? ErrorMessage { get; set; } public int? ErrorCode { get; set; } public static Result<T> Success(T data) => new() { IsSuccess = true, Data = data }; public static Result<T> Failure(string message, int? errorCode = null) => new() { IsSuccess = false, ErrorMessage = message, ErrorCode = errorCode }; }
修改异常处理逻辑后,调用方可以明确知道操作是否成功,以及失败的原因,而不是只能通过返回值是否为空判断。
3. 是否用专门的异常处理服务?
如果你的系统中有大量跨服务重复的异常处理逻辑(比如不同服务都需要处理数据库异常、权限异常等),可以把异常处理抽到专门的服务中,或者使用全局异常中间件(Web项目场景)。但对于这种特定的OracleException 1013(请求取消),属于业务方法的特定处理逻辑,放在服务层或者提取通用方法都是合理的。
如果用专门的异常处理服务,需要传递的信息包括:
- 异常对象本身;
- 操作上下文(比如当前执行的方法名、关键参数信息,方便日志排查);
- 预期的返回值类型(用于生成对应的失败结果)。
内容的提问来源于stack exchange,提问作者Stormhead

