You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何规范组织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 [];
        }
    }
}

技术问询

  1. 我当前对TDto的使用方式是否正确?
  2. GetByIdAsync方法中的include参数违反了单一职责原则(SRP)。假设User存在大量外部依赖,且各处使用方式不同,是否需要针对每种Include组合编写独立方法?
  3. 如何合理组织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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 22:13:19