开发指引

工程结构

所有项目采用单工程结构。不拆分各种模块工程。通用模块由架构团队统一以 Nuget 包形式提供。
发布相关的内容放入 deploy 文件夹。文档相关放到 docs 文件夹中。

dotnet new webapi -n Demo --use-controllers --framework 'net8.0'
├── Dockerfile
├── Jenkinsfile
├── LICENSE
├── NuGet.Config
├── README.md
├── deploy
│   ├── k8s.cm.yaml
│   ├── k8s.deploy.yaml
│   └── k8s.service.yaml
├── docs
├── global.json
└── src
    ├── Controllers
       ├── TestController.cs
       └── TestController.http
    ├── Demo.csproj
    ├── Program.cs
    ├── Properties
       └── launchSettings.json
    ├── SerilogFormatter.cs
    ├── appsettings.Development.json
    ├── appsettings.Staging.json
    └── appsettings.Production.json

命名规范

小技巧

名称统一使用小写字母。如果需要间隔除服务名称外,统一使用短横杠-
一般我们的名称区分度去掉{env}.{service}的前缀后应该控制在3层左右。
尽量避免使用中文拼音简称,除非是业务中的专有名词。

  1. 服务名称

    1. 服务名称应该精炼,简短且表意清晰。例如:productmessagestsorder

    2. 如果系统过大需要对服务进行拆分,可以在服务名称后面加上模块名称。例如:message-devopssts-admin

    3. 已经上了 k8s 集群的应用,应该在 Deployment 中,对自己的应用增加简短的中文描述,以帮助 k8s 运维人员区分应用。

    如下示例:

    1apiVersion: apps/v1
    2kind: Deployment
    3metadata:
    4    name: order
    5    namespace: middle
    6    labels:
    7      desc: 订单服务
    
  2. 消息队列名称

    出于成本考量,一般不同环境会使用相同的消息队列实例,为了避免不同环境的消息队列混乱和测试环境使用了错误的连接配置导致的消费问题,我们需要对消息队列进行命名。
    规则如下 {env}.{service}.{queue}.{version} 。 如:

    • development.order.create.v1

    • staging.order.create.v1

    • production.order.create.v1

  3. redis 缓存 key 名称

    redis key 的间隔,统一使用冒号 : 做层级间隔。这样在管理工具中,将会以文件夹的形式展示,便于我们管理。
    基本规范类似于消息队列名称:

    • {env}:{service}:{key}

    • {env}:{service}:{module}:{key}

  4. consul kv 存储名称

    consul 存储的 key 规则采用了类 url 的规则,当遇到 / 时,管理界面上会出现文件夹。一般我们使用如下规则:

    • {env}.{service}/{key}

    • {env}.{service}/{module}/{key}

编码规范

  1. 变量小驼峰,方法大驼峰,无错误拼写,避免使用拼音简称,除非是业务中的专有名词。

  2. 主逻辑清晰,不要在主逻辑中实现具体的运算逻辑,应该将运算逻辑封装到方法中。

  3. 不要在一行内写太长的代码,包括方法参数过长。及时格式化。(不超出一屏宽度)

  4. 提交 git 说明写清楚,不要提交无意义的代码。多次提交同一个功能代码需要合并到一个 commit 中。

  5. 无用代码不要通过注释的方法保留到代码中,及时删除。

  6. 同一个类型的不同方法中不要出现重复代码.

枚举、状态规范

  1. 避免在代码中使用数字或字符串,应该使用枚举类型。

  2. 请求体及数据库持久化时,枚举类型应该转换为字符串。

  3. Flags 类型的枚举不强制要求。

正确示例

public enum Status
{
    Enabled,
    Disabled,
    Deleted
}

public class RequestInput
{
    public Status Status { get; set; }
}

public async Task<IActionResult> UpdateStatus([FromBody] RequestInput input)
{
    // 业务逻辑
    return Ok();
}
POST /api/v1/state/update HTTP/1.1
Content-Type: application/json

{
    "status": "Deleted"
}

文档及注释规范

重要

在源代码中多写注释

  1. 在项目的顶层文件夹中创建 docs 文件夹。
    将项目的部署架构、接口文档、数据库设计等文档放在该文件夹中。使用 git 来管理文档更变历史记录。

  2. 所有方法都需要增加注释,注释仅包含说明即可。

    /// <summary>
    /// This is annoying cos it should be in the constructor but we need to set up the file before calling startup so its a step.
    /// </summary>
    public void GivenOcelotIsRunning(
        Action<IdentityServerAuthenticationOptions> options,
        string authenticationProviderKey){
        }
    
  3. 一般的业务逻辑注释

    1. 当代码无法通过直接阅读而清晰获理解它的功能的时候,需要加注释。

      if (password == "12345") // 这是超级密码,通过后拥有管理员权限
      
    2. 代码片段有单一功能但没有分割成函数,需要增加注释

      // 添加用户
      var user = new User();
      _userRepository.Add(user);
      
      // 添加用户组
      var group = _userGroupRepository.Get(groupId);
      group.Users.Add(user);
      _userGroupRepository.Update(group);
      
    3. 嵌套、分支注释

      // 当一个嵌套很长,或者有多个嵌套时,需要在结尾加上注释
      if(a>b)  // a > b
      {
      } else if (a>c) // a > c
      {
      
      } else if (b>c) // b > c
      {
      
      } else{
          // todo : throw err
      }
      

程序错误处理

建议 Action 都返回 IActionResult 类型,业务层的错误都通过抛出异常的方式处理,Controller 中针对不同的错误类型返回不同的 IActionResult
前端代码需要正对 4xx5xx 等错误给出友好的提示。

常见需要处理的错误类型:

  • 用户输入验证:当用户输入的数据不符合应用程序要求的格式或范围时。

  • 数据模型验证:在尝试将数据保存到数据库之前,确保数据符合模型的约束。

  • 业务规则验证:当数据不满足特定的业务逻辑或规则时。

  • 系统未捕获的异常:当应用程序中发生未处理的异常时。

[HttpGet]
[Route("api/problem")]
public IActionResult Problem()
{
    try
    {
        // business logic
        var error = false;
        if (error)
        {
            throw new ValidationException("业务规则错误提示!");
        }

        return Ok(new { data = "ok" });
    }
    catch (Exception ex)
    {
        return ex is ValidationException
            // Microsoft.AspNetCore.Http.StatusCodes.Status400BadRequest
            // 返回 400 状态码
            ? ValidationProblem(ex.Message)
            // Microsoft.AspNetCore.Http.StatusCodes.Status500InternalServerError
            // 返回 500 状态码
            : Problem(ex.Message);
    }

}

ValidationProblemProblem 方法会返回统一的数据结构( Microsoft.AspNetCore.Mvc.ProblemDetails ),前端可以根据返回的数据结构进行错误提示。

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "detail": "业务规则错误提示!",
  "traceId": "00-3ab8505c3d7ca10143c5b881cd7423dc-d639bddc5cfd3972-00",
  "errors": {}
}

详情参见微软官方文档 action-return-typeshandle errors

全局异常处理

目前 dotnet core 框架中,推荐使用 IExceptionHandler 来实现全局的异常处理。

public class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger<GlobalExceptionHandler> _logger;

    public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
    {
        _logger = logger;
    }

    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        var problemDetails = new ProblemDetails()
        {
            Title = "System Unhandled Exception",
            Detail = ExceptionMsg.SystemErr,
            Status = (int)HttpStatusCode.InternalServerError,
        };

        if (exception is BizException)
        {
            problemDetails.Title = "Business Exception";
            problemDetails.Detail = exception.Message;
            problemDetails.Status = (int)HttpStatusCode.BadRequest;
        }

        var result = new ObjectResult(problemDetails)
        {
            StatusCode = problemDetails.Status,
            DeclaredType = typeof(ProblemDetails)
        };

        httpContext.Response.ContentType = "application/json; charset=utf-8";
        httpContext.Response.StatusCode = problemDetails.Status.Value;

        await result.ExecuteResultAsync(new ActionContext
        {
            HttpContext = httpContext,
            RouteData = httpContext.GetRouteData(),
            ActionDescriptor = new Microsoft.AspNetCore.Mvc.Abstractions.ActionDescriptor()
        });

        return await ValueTask.FromResult(true);
    }
}
 1// Program.cs
 2var builder = WebApplication.CreateBuilder(args);
 3var config = builder.Configuration;
 4builder.Services.AddProblemDetails();
 5builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
 6var app = builder.Build();
 7app.UseExceptionHandler();
 8app.UseAuthorization();
 9app.MapControllers();
10app.Run();

小技巧

Action 上的入参模型校验会比自定义的 GlobalExceptionHandler 提前执行。所以不受自定义影响。

Git 操作规范

开发人员开发一个子任务的分支过程:

# 0 切换到dev分支
git checkout dev
# 1 获取dev最新代码
git pull --all -p
# 2 创建新分支 分支名用jira编号或者任务内容描述命名
git checkout -b [new-branch]
# 3 改代码...

# 4 将所有新的改动被git管理
git add .
# 5 提交commit,并对本次提交做任务描述
git commit -a -m 'message'
# 6 更新本地git数据库记录
git fetch --all -p
# 7 合并远程dev代码到本地分支,并解决冲突。
git rebase origin/dev
# 8 将本地分支代码推送至 gitlab
git push -u origin new-branch
# 9 前往 gitlab 发起 Merge Request 给 code review 负责人

注意,这里我们使用了 rebase 来合并分支而不是 merge,具体内容请阅读:https://git-scm.com/book/zh/v2/Git-分支-变基

Docker 内 DotNet 调试工具的使用

以下内容,主要来源微软官方的2个教程。

# 容器环境下,需要添加额外的 Linux Capabilities 权限以便 dotnet-sdk 有足够的权限获取系统的数据。
securityContext:
  capabilities:
    add:
    - ALL
# 安装对应版本的 dotnet-core sdk
# Debian 10
wget https://packages.microsoft.com/config/debian/10/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
dpkg -i packages-microsoft-prod.deb
apt-get update
apt-get install -y dotnet-sdk-5.0

# 安装全局调试工具。
dotnet tool install --global dotnet-counters --version 5.0
dotnet tool install --global dotnet-dump --version 5.0
dotnet tool install --global dotnet-trace --version 5.0
dotnet tool install -g dotnet-symbol --version 5.0
dotnet tool install -g dotnet-sos  --version 5.0
dotnet-sos install

export PATH="~/.dotnet/tools:$PATH"

定位内存溢出

定位内存溢出问题,我们重点是需要收集到程序的内存GC的快照文件: dump文件。然后再使用分析工具分析内存的引用代码。

# counters
dotnet-counters monitor --process-id 1 --refresh-interval 3 --counters System.Runtime,Microsoft.AspNetCore.Hosting

# memory-leak https://learn.microsoft.com/en-us/dotnet/core/diagnostics/debug-memory-leak
# 收集进程号为 1 的 dump 文件。
createdump -p 1
dotnet-dump collect -p 1

dotnet-dump analyze core_20190430_185145

定位高CPU

定位CPU问题,重点的需要收集运行程序的 trace 数据。然后再使用分析工具分析程序占用cpu的占比。

# high cpu https://learn.microsoft.com/en-us/dotnet/core/diagnostics/debug-highcpu?tabs=linux
dotnet-trace collect --process-id 1 --duration 00:01:00

trace.nettrace 文件从容器中复制到本机。并使用 PerfView 工具分析。