zhuge-workflow 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +801 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
- package/templates/claude/CLAUDE-ccg.md +258 -0
- package/templates/claude/CLAUDE.md +106 -0
- package/templates/claude/commands/ccg/_context.md +152 -0
- package/templates/claude/commands/ccg/spec-impl.md +161 -0
- package/templates/claude/commands/ccg/spec-plan-trellis.md +239 -0
- package/templates/claude/commands/ccg/spec-plan.md +225 -0
- package/templates/claude/commands/ccg/spec-research.md +113 -0
- package/templates/claude/commands/ccg/spec-review.md +127 -0
- package/templates/claude/commands/developer/brainstorm.md +5 -0
- package/templates/claude/commands/developer/design-checklist.md +81 -0
- package/templates/claude/commands/developer/design-doc.md +188 -0
- package/templates/claude/commands/developer/requirement-doc.md +150 -0
- package/templates/claude/commands/developer/requirement-interrogate.md +71 -0
- package/templates/claude/commands/developer/status.md +55 -0
- package/templates/claude/rules/bash-style.md +46 -0
- package/templates/claude/rules/claude-code-defensive.md +99 -0
- package/templates/claude/rules/doc-sync.md +49 -0
- package/templates/claude/rules/ops-safety.md +32 -0
- package/templates/claude/skills/bash-style/SKILL.md +244 -0
- package/templates/claude/skills/brainstorming/SKILL.md +48 -0
- package/templates/claude/skills/dotnet-dev/SKILL.md +250 -0
- package/templates/claude/skills/dotnet-dev/references/dotnet-style.md +991 -0
- package/templates/claude/skills/mcp-builder/LICENSE.txt +202 -0
- package/templates/claude/skills/mcp-builder/SKILL.md +328 -0
- package/templates/claude/skills/mcp-builder/reference/evaluation.md +602 -0
- package/templates/claude/skills/mcp-builder/reference/mcp_best_practices.md +915 -0
- package/templates/claude/skills/mcp-builder/reference/node_mcp_server.md +916 -0
- package/templates/claude/skills/mcp-builder/reference/python_mcp_server.md +752 -0
- package/templates/claude/skills/mcp-builder/scripts/connections.py +151 -0
- package/templates/claude/skills/mcp-builder/scripts/evaluation.py +373 -0
- package/templates/claude/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
- package/templates/claude/skills/mcp-builder/scripts/requirements.txt +2 -0
- package/templates/claude/skills/ops-safety/SKILL.md +130 -0
- package/templates/claude/skills/python-dev/SKILL.md +281 -0
- package/templates/claude/skills/skill-creator/LICENSE.txt +202 -0
- package/templates/claude/skills/skill-creator/SKILL.md +209 -0
- package/templates/claude/skills/skill-creator/scripts/init_skill.py +303 -0
- package/templates/claude/skills/skill-creator/scripts/package_skill.py +110 -0
- package/templates/claude/skills/skill-creator/scripts/quick_validate.py +65 -0
- package/templates/claude/skills/sqlserver-executor/SKILL.md +201 -0
- package/templates/claude/skills/sqlserver-executor/assets/config-example.json +26 -0
- package/templates/claude/skills/sqlserver-executor/config.json +12 -0
- package/templates/claude/skills/sqlserver-executor/scripts/sql_executor.py +404 -0
- package/templates/claude/skills/ui-ux-pro-max/SKILL.md +228 -0
- package/templates/claude/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/templates/claude/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/templates/claude/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/templates/claude/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/templates/claude/skills/ui-ux-pro-max/data/prompts.csv +24 -0
- package/templates/claude/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/templates/claude/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/templates/claude/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/templates/claude/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/templates/claude/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/templates/claude/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/templates/claude/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/templates/claude/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/templates/claude/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/templates/claude/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/templates/claude/skills/ui-ux-pro-max/data/styles.csv +59 -0
- package/templates/claude/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/templates/claude/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/templates/claude/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-313.pyc +0 -0
- package/templates/claude/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-314.pyc +0 -0
- package/templates/claude/skills/ui-ux-pro-max/scripts/core.py +238 -0
- package/templates/claude/skills/ui-ux-pro-max/scripts/search.py +61 -0
- package/templates/claude/skills/webapp-testing/LICENSE.txt +202 -0
- package/templates/claude/skills/webapp-testing/SKILL.md +96 -0
- package/templates/claude/skills/webapp-testing/examples/console_logging.py +35 -0
- package/templates/claude/skills/webapp-testing/examples/element_discovery.py +40 -0
- package/templates/claude/skills/webapp-testing/examples/static_html_automation.py +33 -0
- package/templates/claude/skills/webapp-testing/scripts/with_server.py +106 -0
- package/templates/init/claude-agents/ccg-impl.md +199 -0
- package/templates/init/claude-agents/ccg-review.md +146 -0
- package/templates/init/claude-agents/dispatch.md +253 -0
- package/templates/init/claude-hooks/inject-subagent-context.py +964 -0
- package/templates/init/trellis-scripts/task.sh +1326 -0
|
@@ -0,0 +1,991 @@
|
|
|
1
|
+
# .NET Core 开发规范
|
|
2
|
+
|
|
3
|
+
作者:wwj
|
|
4
|
+
版本:v1.0
|
|
5
|
+
日期:2026-01-13
|
|
6
|
+
状态:草稿
|
|
7
|
+
|
|
8
|
+
> **部署位置**: `~/.claude/skills/dotnet-dev/`
|
|
9
|
+
> **作用范围**: 所有 .NET Core 项目
|
|
10
|
+
> **参考来源**: Microsoft C# 编码约定、Furion 官方文档、SqlSugar 官方文档
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
paths:
|
|
14
|
+
- "**/*.cs"
|
|
15
|
+
- "**/*.csproj"
|
|
16
|
+
- "**/*.sln"
|
|
17
|
+
- "**/appsettings.json"
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 工具链
|
|
21
|
+
|
|
22
|
+
<!-- [注释] 可根据项目调整 -->
|
|
23
|
+
|
|
24
|
+
- 格式化: dotnet format / IDE 内置格式化
|
|
25
|
+
- 静态检查: Roslyn Analyzers、StyleCop.Analyzers
|
|
26
|
+
- 构建工具: dotnet CLI / MSBuild
|
|
27
|
+
- 测试: xUnit + Moq
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# dotnet CLI 常用命令
|
|
31
|
+
dotnet build # 编译
|
|
32
|
+
dotnet test # 运行测试
|
|
33
|
+
dotnet run # 运行项目
|
|
34
|
+
dotnet publish -c Release # 发布
|
|
35
|
+
dotnet format # 格式化代码
|
|
36
|
+
|
|
37
|
+
# 带分析器的构建
|
|
38
|
+
dotnet build /p:TreatWarningsAsErrors=true
|
|
39
|
+
dotnet build /p:EnforceCodeStyleInBuild=true
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 命名约定
|
|
43
|
+
|
|
44
|
+
<!-- [注释] 遵循 Microsoft 官方规范 -->
|
|
45
|
+
|
|
46
|
+
### 命名空间
|
|
47
|
+
- PascalCase,公司.产品.模块: `MyCompany.Project.Services`
|
|
48
|
+
- 避免与类名冲突
|
|
49
|
+
|
|
50
|
+
```csharp
|
|
51
|
+
// ✅ 好
|
|
52
|
+
namespace MyCompany.ECommerce.Services;
|
|
53
|
+
namespace MyCompany.ECommerce.Models;
|
|
54
|
+
|
|
55
|
+
// ❌ 差
|
|
56
|
+
namespace mycompany.ecommerce; // 应使用 PascalCase
|
|
57
|
+
namespace My_Company.E_Commerce; // 不要用下划线
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 类命名
|
|
61
|
+
- PascalCase: `UserService`、`HttpClient`
|
|
62
|
+
- 类名应是名词或名词短语
|
|
63
|
+
- 接口以 I 前缀: `IUserRepository`、`IDisposable`
|
|
64
|
+
|
|
65
|
+
```csharp
|
|
66
|
+
// ✅ 好
|
|
67
|
+
public class UserService { }
|
|
68
|
+
public class HttpRequestHandler { }
|
|
69
|
+
public interface IUserRepository { }
|
|
70
|
+
|
|
71
|
+
// ❌ 差
|
|
72
|
+
public class userService { } // 应大写开头
|
|
73
|
+
public class Do_Something { } // 不要用下划线
|
|
74
|
+
public interface UserRepository { } // 接口应以 I 开头
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 方法命名
|
|
78
|
+
- PascalCase: `GetUserById`、`IsValid`
|
|
79
|
+
- 动词或动词短语开头
|
|
80
|
+
- 异步方法以 Async 后缀: `GetUserByIdAsync`
|
|
81
|
+
|
|
82
|
+
```csharp
|
|
83
|
+
// ✅ 好
|
|
84
|
+
public User FindById(long id) { }
|
|
85
|
+
public async Task<User> FindByIdAsync(long id) { }
|
|
86
|
+
public bool IsActive() { }
|
|
87
|
+
public bool HasPermission(string role) { }
|
|
88
|
+
|
|
89
|
+
// ❌ 差
|
|
90
|
+
public User findById(long id) { } // 应 PascalCase
|
|
91
|
+
public async Task<User> FindById() { } // 异步应加 Async 后缀
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 字段与属性命名
|
|
95
|
+
- 公共属性: PascalCase `UserId`
|
|
96
|
+
- 私有字段: _camelCase `_userId`
|
|
97
|
+
- 常量: PascalCase `MaxRetryCount`
|
|
98
|
+
- 静态只读: PascalCase `DefaultTimeout`
|
|
99
|
+
|
|
100
|
+
```csharp
|
|
101
|
+
// ✅ 好
|
|
102
|
+
private readonly long _userId;
|
|
103
|
+
private readonly IUserRepository _userRepository;
|
|
104
|
+
public long UserId { get; set; }
|
|
105
|
+
public const int MaxRetryCount = 3;
|
|
106
|
+
public static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(30);
|
|
107
|
+
|
|
108
|
+
// ❌ 差
|
|
109
|
+
private long userId; // 私有字段应加下划线前缀
|
|
110
|
+
public long userId { get; } // 属性应 PascalCase
|
|
111
|
+
private const int MAX_RETRY; // 常量不用全大写
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 泛型类型参数
|
|
115
|
+
- 单个大写字母或描述性名称: `T`、`TEntity`、`TKey`、`TValue`
|
|
116
|
+
|
|
117
|
+
```csharp
|
|
118
|
+
// ✅ 好
|
|
119
|
+
public class Repository<TEntity> where TEntity : class { }
|
|
120
|
+
public interface IDictionary<TKey, TValue> { }
|
|
121
|
+
public T Find<T>(int id) where T : class { }
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## 代码组织
|
|
125
|
+
|
|
126
|
+
### 类成员顺序
|
|
127
|
+
|
|
128
|
+
<!-- [注释] 建议顺序,可根据团队习惯调整 -->
|
|
129
|
+
|
|
130
|
+
```csharp
|
|
131
|
+
public class Example
|
|
132
|
+
{
|
|
133
|
+
// 1. 常量
|
|
134
|
+
public const string DefaultName = "value";
|
|
135
|
+
|
|
136
|
+
// 2. 静态只读字段
|
|
137
|
+
private static readonly ILogger<Example> _logger;
|
|
138
|
+
|
|
139
|
+
// 3. 静态字段
|
|
140
|
+
private static int _instanceCount;
|
|
141
|
+
|
|
142
|
+
// 4. 实例只读字段
|
|
143
|
+
private readonly IUserRepository _userRepository;
|
|
144
|
+
|
|
145
|
+
// 5. 实例字段
|
|
146
|
+
private long _id;
|
|
147
|
+
|
|
148
|
+
// 6. 构造函数
|
|
149
|
+
public Example() { }
|
|
150
|
+
public Example(IUserRepository userRepository)
|
|
151
|
+
{
|
|
152
|
+
_userRepository = userRepository;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 7. 属性
|
|
156
|
+
public long Id { get; set; }
|
|
157
|
+
public string Name { get; init; }
|
|
158
|
+
|
|
159
|
+
// 8. 公共方法
|
|
160
|
+
public void DoSomething() { }
|
|
161
|
+
public async Task DoSomethingAsync() { }
|
|
162
|
+
|
|
163
|
+
// 9. 私有方法
|
|
164
|
+
private void HelperMethod() { }
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### using 规范
|
|
169
|
+
- System 命名空间优先
|
|
170
|
+
- 按字母顺序排列
|
|
171
|
+
- 使用 global using 减少重复
|
|
172
|
+
|
|
173
|
+
```csharp
|
|
174
|
+
// ✅ 好 - 文件顶部
|
|
175
|
+
using System;
|
|
176
|
+
using System.Collections.Generic;
|
|
177
|
+
using System.Threading.Tasks;
|
|
178
|
+
|
|
179
|
+
using Microsoft.AspNetCore.Mvc;
|
|
180
|
+
using Microsoft.Extensions.Logging;
|
|
181
|
+
|
|
182
|
+
using MyCompany.Project.Models;
|
|
183
|
+
using MyCompany.Project.Services;
|
|
184
|
+
|
|
185
|
+
// ✅ 好 - GlobalUsings.cs
|
|
186
|
+
global using System;
|
|
187
|
+
global using System.Collections.Generic;
|
|
188
|
+
global using Microsoft.Extensions.Logging;
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### 项目结构
|
|
192
|
+
|
|
193
|
+
<!-- [注释] 遵循 Clean Architecture 或分层架构 -->
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
Project/
|
|
197
|
+
├── src/
|
|
198
|
+
│ ├── Project.Api/ # Web API 层
|
|
199
|
+
│ │ ├── Controllers/
|
|
200
|
+
│ │ ├── Filters/
|
|
201
|
+
│ │ ├── Middleware/
|
|
202
|
+
│ │ └── Program.cs
|
|
203
|
+
│ ├── Project.Application/ # 应用层
|
|
204
|
+
│ │ ├── Services/
|
|
205
|
+
│ │ ├── DTOs/
|
|
206
|
+
│ │ └── Interfaces/
|
|
207
|
+
│ ├── Project.Domain/ # 领域层
|
|
208
|
+
│ │ ├── Entities/
|
|
209
|
+
│ │ ├── ValueObjects/
|
|
210
|
+
│ │ └── Interfaces/
|
|
211
|
+
│ └── Project.Infrastructure/ # 基础设施层
|
|
212
|
+
│ ├── Data/
|
|
213
|
+
│ │ ├── DbContext.cs
|
|
214
|
+
│ │ └── Repositories/
|
|
215
|
+
│ └── Services/
|
|
216
|
+
├── tests/
|
|
217
|
+
│ ├── Project.UnitTests/
|
|
218
|
+
│ └── Project.IntegrationTests/
|
|
219
|
+
└── Project.sln
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## 异常处理
|
|
223
|
+
|
|
224
|
+
<!-- [注释] 异常处理是 .NET 开发的重点 -->
|
|
225
|
+
|
|
226
|
+
### 基本原则
|
|
227
|
+
- 优先使用内置异常类型
|
|
228
|
+
- 不要捕获 `Exception`(除非在最顶层)
|
|
229
|
+
- 不要忽略异常(空 catch 块)
|
|
230
|
+
- 异常信息要有意义
|
|
231
|
+
|
|
232
|
+
```csharp
|
|
233
|
+
// ✅ 好:捕获具体异常,添加上下文
|
|
234
|
+
try
|
|
235
|
+
{
|
|
236
|
+
user = await _userRepository.FindByIdAsync(id);
|
|
237
|
+
}
|
|
238
|
+
catch (DbUpdateException ex)
|
|
239
|
+
{
|
|
240
|
+
throw new ServiceException($"Failed to find user: {id}", ex);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ✅ 好:资源自动释放
|
|
244
|
+
await using var stream = File.OpenRead(filePath);
|
|
245
|
+
await using var connection = new SqlConnection(connectionString);
|
|
246
|
+
|
|
247
|
+
// ❌ 差:捕获过宽
|
|
248
|
+
try
|
|
249
|
+
{
|
|
250
|
+
DoSomething();
|
|
251
|
+
}
|
|
252
|
+
catch (Exception ex) // 太宽泛
|
|
253
|
+
{
|
|
254
|
+
Console.WriteLine(ex); // 不要用 Console.WriteLine
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ❌ 差:忽略异常
|
|
258
|
+
try
|
|
259
|
+
{
|
|
260
|
+
DoSomething();
|
|
261
|
+
}
|
|
262
|
+
catch (IOException)
|
|
263
|
+
{
|
|
264
|
+
// 空的 catch 块,异常被吞掉
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### 自定义异常
|
|
269
|
+
- 业务异常继承 `Exception`
|
|
270
|
+
- 必须提供有意义的消息
|
|
271
|
+
- 实现序列化构造函数(如需跨进程传递)
|
|
272
|
+
|
|
273
|
+
```csharp
|
|
274
|
+
public class BusinessException : Exception
|
|
275
|
+
{
|
|
276
|
+
public string ErrorCode { get; }
|
|
277
|
+
|
|
278
|
+
public BusinessException(string errorCode, string message)
|
|
279
|
+
: base(message)
|
|
280
|
+
{
|
|
281
|
+
ErrorCode = errorCode;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
public BusinessException(string errorCode, string message, Exception innerException)
|
|
285
|
+
: base(message, innerException)
|
|
286
|
+
{
|
|
287
|
+
ErrorCode = errorCode;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 使用
|
|
292
|
+
throw new BusinessException("USER_NOT_FOUND", $"User with id {id} not found");
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## 空值处理
|
|
296
|
+
|
|
297
|
+
<!-- [注释] NRE 是最常见的错误,使用 Nullable Reference Types -->
|
|
298
|
+
|
|
299
|
+
### 基本原则
|
|
300
|
+
- 启用 Nullable Reference Types (`<Nullable>enable</Nullable>`)
|
|
301
|
+
- 使用 `?` 标记可空类型
|
|
302
|
+
- 参数校验放在方法开头
|
|
303
|
+
|
|
304
|
+
```csharp
|
|
305
|
+
// ✅ 好:启用 Nullable Reference Types
|
|
306
|
+
public User? FindById(long id)
|
|
307
|
+
{
|
|
308
|
+
return _context.Users.FirstOrDefault(u => u.Id == id);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ✅ 好:参数校验
|
|
312
|
+
public void UpdateUser(User user)
|
|
313
|
+
{
|
|
314
|
+
ArgumentNullException.ThrowIfNull(user);
|
|
315
|
+
ArgumentNullException.ThrowIfNull(user.Id);
|
|
316
|
+
// ...
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ✅ 好:安全的空值处理
|
|
320
|
+
var name = user?.Name ?? "Unknown";
|
|
321
|
+
var length = user?.Name?.Length ?? 0;
|
|
322
|
+
|
|
323
|
+
// ✅ 好:模式匹配
|
|
324
|
+
if (user is { Name: var name, Age: > 18 })
|
|
325
|
+
{
|
|
326
|
+
Console.WriteLine($"Adult user: {name}");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ❌ 差:返回 null 且未标记可空
|
|
330
|
+
public User FindById(long id) // 应该是 User?
|
|
331
|
+
{
|
|
332
|
+
return _context.Users.FirstOrDefault(u => u.Id == id);
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## 注释规范
|
|
337
|
+
|
|
338
|
+
<!-- [注释] XML 文档注释是 .NET 文档的标准方式 -->
|
|
339
|
+
|
|
340
|
+
### XML 文档注释
|
|
341
|
+
- 所有公共 API 应有文档注释
|
|
342
|
+
- 描述"做什么"而非"怎么做"
|
|
343
|
+
|
|
344
|
+
```csharp
|
|
345
|
+
/// <summary>
|
|
346
|
+
/// Finds a user by their unique identifier.
|
|
347
|
+
/// </summary>
|
|
348
|
+
/// <param name="id">The user's unique identifier.</param>
|
|
349
|
+
/// <returns>The user if found; otherwise, null.</returns>
|
|
350
|
+
/// <exception cref="ArgumentException">Thrown when id is less than 1.</exception>
|
|
351
|
+
public async Task<User?> FindByIdAsync(long id)
|
|
352
|
+
{
|
|
353
|
+
if (id < 1)
|
|
354
|
+
throw new ArgumentException("Id must be positive", nameof(id));
|
|
355
|
+
|
|
356
|
+
return await _context.Users.FindAsync(id);
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### 行内注释
|
|
361
|
+
- 解释"为什么"而非"是什么"
|
|
362
|
+
- 避免废话注释
|
|
363
|
+
|
|
364
|
+
```csharp
|
|
365
|
+
// ✅ 好:解释原因
|
|
366
|
+
// 使用 lock 而非 ConcurrentDictionary,因为需要原子地检查并更新多个字段
|
|
367
|
+
lock (_syncLock)
|
|
368
|
+
{
|
|
369
|
+
// ...
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ❌ 差:废话注释
|
|
373
|
+
// 获取用户 ID
|
|
374
|
+
var userId = user.Id; // 代码已经很清楚了
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
## 异步编程
|
|
378
|
+
|
|
379
|
+
<!-- [注释] async/await 是 .NET 异步编程的核心 -->
|
|
380
|
+
|
|
381
|
+
### 基本原则
|
|
382
|
+
- IO 操作使用 async/await
|
|
383
|
+
- 不要阻塞异步调用(`.Result`、`.Wait()`)
|
|
384
|
+
- 正确传递 CancellationToken
|
|
385
|
+
|
|
386
|
+
```csharp
|
|
387
|
+
// ✅ 好:使用 async/await
|
|
388
|
+
public async Task<User?> GetUserAsync(long id, CancellationToken cancellationToken = default)
|
|
389
|
+
{
|
|
390
|
+
return await _context.Users
|
|
391
|
+
.FirstOrDefaultAsync(u => u.Id == id, cancellationToken);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ✅ 好:并行执行
|
|
395
|
+
var tasks = userIds.Select(id => GetUserAsync(id));
|
|
396
|
+
var users = await Task.WhenAll(tasks);
|
|
397
|
+
|
|
398
|
+
// ✅ 好:带超时的操作
|
|
399
|
+
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
|
|
400
|
+
var result = await GetDataAsync(cts.Token);
|
|
401
|
+
|
|
402
|
+
// ❌ 差:阻塞异步调用(可能死锁)
|
|
403
|
+
var user = GetUserAsync(id).Result;
|
|
404
|
+
GetUserAsync(id).Wait();
|
|
405
|
+
|
|
406
|
+
// ❌ 差:async void(除了事件处理器)
|
|
407
|
+
public async void DoSomething() { } // 应该返回 Task
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### 并发控制
|
|
411
|
+
|
|
412
|
+
```csharp
|
|
413
|
+
// ✅ 使用 SemaphoreSlim 限制并发
|
|
414
|
+
private readonly SemaphoreSlim _semaphore = new(10);
|
|
415
|
+
|
|
416
|
+
public async Task ProcessAsync()
|
|
417
|
+
{
|
|
418
|
+
await _semaphore.WaitAsync();
|
|
419
|
+
try
|
|
420
|
+
{
|
|
421
|
+
await DoWorkAsync();
|
|
422
|
+
}
|
|
423
|
+
finally
|
|
424
|
+
{
|
|
425
|
+
_semaphore.Release();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ✅ 使用 Channel 进行生产者-消费者
|
|
430
|
+
var channel = Channel.CreateBounded<WorkItem>(100);
|
|
431
|
+
|
|
432
|
+
// Producer
|
|
433
|
+
await channel.Writer.WriteAsync(item);
|
|
434
|
+
|
|
435
|
+
// Consumer
|
|
436
|
+
await foreach (var item in channel.Reader.ReadAllAsync())
|
|
437
|
+
{
|
|
438
|
+
await ProcessItemAsync(item);
|
|
439
|
+
}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
## 测试规范
|
|
443
|
+
|
|
444
|
+
<!-- [注释] 使用 xUnit + Moq -->
|
|
445
|
+
|
|
446
|
+
### 测试方法命名
|
|
447
|
+
- 描述测试场景和预期结果
|
|
448
|
+
- 格式: `MethodName_Scenario_ExpectedResult`
|
|
449
|
+
|
|
450
|
+
```csharp
|
|
451
|
+
public class UserServiceTests
|
|
452
|
+
{
|
|
453
|
+
private readonly Mock<IUserRepository> _mockRepository;
|
|
454
|
+
private readonly UserService _sut; // System Under Test
|
|
455
|
+
|
|
456
|
+
public UserServiceTests()
|
|
457
|
+
{
|
|
458
|
+
_mockRepository = new Mock<IUserRepository>();
|
|
459
|
+
_sut = new UserService(_mockRepository.Object);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
[Fact]
|
|
463
|
+
public async Task FindByIdAsync_WhenUserExists_ReturnsUser()
|
|
464
|
+
{
|
|
465
|
+
// Arrange
|
|
466
|
+
var expected = new User { Id = 1, Name = "test" };
|
|
467
|
+
_mockRepository
|
|
468
|
+
.Setup(r => r.FindByIdAsync(1))
|
|
469
|
+
.ReturnsAsync(expected);
|
|
470
|
+
|
|
471
|
+
// Act
|
|
472
|
+
var result = await _sut.FindByIdAsync(1);
|
|
473
|
+
|
|
474
|
+
// Assert
|
|
475
|
+
Assert.NotNull(result);
|
|
476
|
+
Assert.Equal("test", result.Name);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
[Fact]
|
|
480
|
+
public async Task FindByIdAsync_WhenUserNotExists_ReturnsNull()
|
|
481
|
+
{
|
|
482
|
+
// Arrange
|
|
483
|
+
_mockRepository
|
|
484
|
+
.Setup(r => r.FindByIdAsync(It.IsAny<long>()))
|
|
485
|
+
.ReturnsAsync((User?)null);
|
|
486
|
+
|
|
487
|
+
// Act
|
|
488
|
+
var result = await _sut.FindByIdAsync(999);
|
|
489
|
+
|
|
490
|
+
// Assert
|
|
491
|
+
Assert.Null(result);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
[Theory]
|
|
495
|
+
[InlineData(0)]
|
|
496
|
+
[InlineData(-1)]
|
|
497
|
+
public async Task FindByIdAsync_WhenIdInvalid_ThrowsArgumentException(long id)
|
|
498
|
+
{
|
|
499
|
+
// Act & Assert
|
|
500
|
+
await Assert.ThrowsAsync<ArgumentException>(
|
|
501
|
+
() => _sut.FindByIdAsync(id));
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### 测试结构
|
|
507
|
+
- 使用 Arrange-Act-Assert 模式
|
|
508
|
+
- 每个测试只验证一个行为
|
|
509
|
+
|
|
510
|
+
```csharp
|
|
511
|
+
[Fact]
|
|
512
|
+
public async Task CreateOrder_WithValidData_CreatesAndReturnsOrder()
|
|
513
|
+
{
|
|
514
|
+
// Arrange
|
|
515
|
+
var request = new CreateOrderRequest { /* ... */ };
|
|
516
|
+
_mockProductService
|
|
517
|
+
.Setup(s => s.CheckStockAsync(It.IsAny<long>()))
|
|
518
|
+
.ReturnsAsync(true);
|
|
519
|
+
|
|
520
|
+
// Act
|
|
521
|
+
var result = await _sut.CreateOrderAsync(request);
|
|
522
|
+
|
|
523
|
+
// Assert
|
|
524
|
+
Assert.NotNull(result);
|
|
525
|
+
Assert.Equal(OrderStatus.Created, result.Status);
|
|
526
|
+
_mockOrderRepository.Verify(r => r.AddAsync(It.IsAny<Order>()), Times.Once);
|
|
527
|
+
}
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
## 日志规范
|
|
531
|
+
|
|
532
|
+
<!-- [注释] 使用 Microsoft.Extensions.Logging 或 Serilog -->
|
|
533
|
+
|
|
534
|
+
### 基本原则
|
|
535
|
+
- 使用结构化日志
|
|
536
|
+
- 使用消息模板,避免字符串拼接
|
|
537
|
+
- 选择合适的日志级别
|
|
538
|
+
|
|
539
|
+
```csharp
|
|
540
|
+
// ✅ 好:结构化日志
|
|
541
|
+
private readonly ILogger<UserService> _logger;
|
|
542
|
+
|
|
543
|
+
_logger.LogDebug("Finding user by id: {UserId}", userId);
|
|
544
|
+
_logger.LogInformation("User {Username} logged in from {IpAddress}", username, ip);
|
|
545
|
+
_logger.LogWarning("Failed to send email to {Email}, will retry", email);
|
|
546
|
+
_logger.LogError(exception, "Failed to process order {OrderId}", orderId);
|
|
547
|
+
|
|
548
|
+
// ✅ 好:高性能日志(.NET 6+)
|
|
549
|
+
[LoggerMessage(Level = LogLevel.Information, Message = "User {UserId} logged in")]
|
|
550
|
+
partial void LogUserLogin(long userId);
|
|
551
|
+
|
|
552
|
+
// ❌ 差:字符串拼接(即使不输出也会执行拼接)
|
|
553
|
+
_logger.LogDebug("Finding user by id: " + userId);
|
|
554
|
+
_logger.LogDebug($"Finding user by id: {userId}");
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### 日志级别
|
|
558
|
+
- `Critical`: 系统崩溃,需要立即处理
|
|
559
|
+
- `Error`: 操作失败,需要关注
|
|
560
|
+
- `Warning`: 警告,可能的问题
|
|
561
|
+
- `Information`: 重要业务事件
|
|
562
|
+
- `Debug`: 调试信息
|
|
563
|
+
- `Trace`: 详细追踪信息
|
|
564
|
+
|
|
565
|
+
## Furion 框架规范
|
|
566
|
+
|
|
567
|
+
<!-- [注释] Furion 最佳实践 -->
|
|
568
|
+
|
|
569
|
+
### 动态 API
|
|
570
|
+
- 使用 `IDynamicApiController` 自动生成 RESTful API
|
|
571
|
+
- 方法名自动映射为 HTTP 动词
|
|
572
|
+
|
|
573
|
+
```csharp
|
|
574
|
+
// ✅ 好:动态 API(自动生成路由)
|
|
575
|
+
[DynamicApiController]
|
|
576
|
+
public class UserService : IDynamicApiController
|
|
577
|
+
{
|
|
578
|
+
private readonly ISqlSugarRepository<User> _repository;
|
|
579
|
+
|
|
580
|
+
public UserService(ISqlSugarRepository<User> repository)
|
|
581
|
+
{
|
|
582
|
+
_repository = repository;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// GET /api/user/{id}
|
|
586
|
+
public async Task<User> GetAsync(long id)
|
|
587
|
+
{
|
|
588
|
+
return await _repository.GetByIdAsync(id);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// GET /api/user/list
|
|
592
|
+
public async Task<List<User>> GetListAsync()
|
|
593
|
+
{
|
|
594
|
+
return await _repository.GetListAsync();
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// POST /api/user
|
|
598
|
+
public async Task<long> AddAsync(CreateUserDto dto)
|
|
599
|
+
{
|
|
600
|
+
var user = dto.Adapt<User>();
|
|
601
|
+
return await _repository.InsertReturnIdentityAsync(user);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// PUT /api/user
|
|
605
|
+
public async Task UpdateAsync(UpdateUserDto dto)
|
|
606
|
+
{
|
|
607
|
+
var user = dto.Adapt<User>();
|
|
608
|
+
await _repository.UpdateAsync(user);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// DELETE /api/user/{id}
|
|
612
|
+
public async Task DeleteAsync(long id)
|
|
613
|
+
{
|
|
614
|
+
await _repository.DeleteByIdAsync(id);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
### 依赖注入
|
|
620
|
+
- Furion 自动扫描并注册服务
|
|
621
|
+
- 使用接口约定:`ITransient`、`IScoped`、`ISingleton`
|
|
622
|
+
|
|
623
|
+
```csharp
|
|
624
|
+
// ✅ 好:使用接口约定自动注册
|
|
625
|
+
public class UserService : IUserService, ITransient
|
|
626
|
+
{
|
|
627
|
+
private readonly ISqlSugarRepository<User> _repository;
|
|
628
|
+
|
|
629
|
+
public UserService(ISqlSugarRepository<User> repository)
|
|
630
|
+
{
|
|
631
|
+
_repository = repository;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// ✅ 好:手动注册(需要更多控制时)
|
|
636
|
+
services.AddScoped<IUserService, UserService>();
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
### 统一返回与异常处理
|
|
640
|
+
|
|
641
|
+
```csharp
|
|
642
|
+
// ✅ 好:使用 Furion 统一返回格式
|
|
643
|
+
[DynamicApiController]
|
|
644
|
+
public class UserService : IDynamicApiController
|
|
645
|
+
{
|
|
646
|
+
// 自动包装为 { code: 200, data: {...}, message: "success" }
|
|
647
|
+
public async Task<User> GetAsync(long id)
|
|
648
|
+
{
|
|
649
|
+
var user = await _repository.GetByIdAsync(id);
|
|
650
|
+
return user ?? throw Oops.Oh("用户不存在");
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// ✅ 好:友好异常
|
|
655
|
+
throw Oops.Oh(ErrorCodes.UserNotFound);
|
|
656
|
+
throw Oops.Bah("业务异常提示");
|
|
657
|
+
|
|
658
|
+
// ✅ 好:自定义错误码
|
|
659
|
+
[ErrorCodeType]
|
|
660
|
+
public enum ErrorCodes
|
|
661
|
+
{
|
|
662
|
+
[ErrorCodeItemMetadata("用户不存在")]
|
|
663
|
+
UserNotFound,
|
|
664
|
+
|
|
665
|
+
[ErrorCodeItemMetadata("用户名已存在")]
|
|
666
|
+
UserNameExists
|
|
667
|
+
}
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
### 数据验证
|
|
671
|
+
|
|
672
|
+
```csharp
|
|
673
|
+
// ✅ 好:使用 DataAnnotations
|
|
674
|
+
public class CreateUserDto
|
|
675
|
+
{
|
|
676
|
+
[Required(ErrorMessage = "用户名不能为空")]
|
|
677
|
+
[MaxLength(50, ErrorMessage = "用户名最长50个字符")]
|
|
678
|
+
public string Name { get; set; }
|
|
679
|
+
|
|
680
|
+
[Required, EmailAddress]
|
|
681
|
+
public string Email { get; set; }
|
|
682
|
+
|
|
683
|
+
[Range(1, 150, ErrorMessage = "年龄范围1-150")]
|
|
684
|
+
public int Age { get; set; }
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// ✅ 好:使用 FluentValidation
|
|
688
|
+
public class CreateUserDtoValidator : AbstractValidator<CreateUserDto>
|
|
689
|
+
{
|
|
690
|
+
public CreateUserDtoValidator()
|
|
691
|
+
{
|
|
692
|
+
RuleFor(x => x.Name).NotEmpty().MaximumLength(50);
|
|
693
|
+
RuleFor(x => x.Email).NotEmpty().EmailAddress();
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
### 传统 Controller(需要更多控制时)
|
|
699
|
+
|
|
700
|
+
```csharp
|
|
701
|
+
[ApiController]
|
|
702
|
+
[Route("api/[controller]")]
|
|
703
|
+
public class UsersController : ControllerBase
|
|
704
|
+
{
|
|
705
|
+
private readonly IUserService _userService;
|
|
706
|
+
|
|
707
|
+
public UsersController(IUserService userService)
|
|
708
|
+
{
|
|
709
|
+
_userService = userService;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
[HttpGet("{id:long}")]
|
|
713
|
+
public async Task<ActionResult<UserDto>> GetById(long id)
|
|
714
|
+
{
|
|
715
|
+
var user = await _userService.GetAsync(id);
|
|
716
|
+
return user is null ? NotFound() : Ok(user);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
## SqlSugar ORM
|
|
721
|
+
|
|
722
|
+
<!-- [注释] SqlSugar 最佳实践 -->
|
|
723
|
+
|
|
724
|
+
### 基础配置
|
|
725
|
+
|
|
726
|
+
```csharp
|
|
727
|
+
// ✅ Program.cs 配置
|
|
728
|
+
builder.Services.AddSqlSugar(new ConnectionConfig
|
|
729
|
+
{
|
|
730
|
+
ConnectionString = builder.Configuration.GetConnectionString("Default"),
|
|
731
|
+
DbType = DbType.MySql,
|
|
732
|
+
IsAutoCloseConnection = true,
|
|
733
|
+
InitKeyType = InitKeyType.Attribute
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
// ✅ 实体定义
|
|
737
|
+
[SugarTable("user")]
|
|
738
|
+
public class User
|
|
739
|
+
{
|
|
740
|
+
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
|
|
741
|
+
public long Id { get; set; }
|
|
742
|
+
|
|
743
|
+
[SugarColumn(Length = 50)]
|
|
744
|
+
public string Name { get; set; }
|
|
745
|
+
|
|
746
|
+
[SugarColumn(IsNullable = true)]
|
|
747
|
+
public string? Email { get; set; }
|
|
748
|
+
|
|
749
|
+
[SugarColumn(IsIgnore = true)] // 忽略映射
|
|
750
|
+
public string FullName => $"{Name}";
|
|
751
|
+
|
|
752
|
+
// 导航属性
|
|
753
|
+
[Navigate(NavigateType.OneToMany, nameof(Order.UserId))]
|
|
754
|
+
public List<Order> Orders { get; set; }
|
|
755
|
+
}
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
### 仓储模式
|
|
759
|
+
|
|
760
|
+
```csharp
|
|
761
|
+
// ✅ 使用内置仓储
|
|
762
|
+
public class UserService : ITransient
|
|
763
|
+
{
|
|
764
|
+
private readonly ISqlSugarRepository<User> _repository;
|
|
765
|
+
|
|
766
|
+
public UserService(ISqlSugarRepository<User> repository)
|
|
767
|
+
{
|
|
768
|
+
_repository = repository;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
public async Task<User?> GetByIdAsync(long id)
|
|
772
|
+
{
|
|
773
|
+
return await _repository.GetByIdAsync(id);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
public async Task<List<User>> GetListAsync(UserQueryDto query)
|
|
777
|
+
{
|
|
778
|
+
return await _repository.AsQueryable()
|
|
779
|
+
.WhereIF(!string.IsNullOrEmpty(query.Name), u => u.Name.Contains(query.Name))
|
|
780
|
+
.WhereIF(query.Status.HasValue, u => u.Status == query.Status)
|
|
781
|
+
.OrderByDescending(u => u.CreateTime)
|
|
782
|
+
.ToPageListAsync(query.PageIndex, query.PageSize);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
### 查询优化
|
|
788
|
+
|
|
789
|
+
```csharp
|
|
790
|
+
// ❌ N+1 查询问题
|
|
791
|
+
var users = await _db.Queryable<User>().ToListAsync();
|
|
792
|
+
foreach (var user in users)
|
|
793
|
+
{
|
|
794
|
+
var orders = await _db.Queryable<Order>().Where(o => o.UserId == user.Id).ToListAsync();
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// ✅ 使用 Includes 导航查询
|
|
798
|
+
var users = await _db.Queryable<User>()
|
|
799
|
+
.Includes(u => u.Orders)
|
|
800
|
+
.ToListAsync();
|
|
801
|
+
|
|
802
|
+
// ✅ 多级导航
|
|
803
|
+
var users = await _db.Queryable<User>()
|
|
804
|
+
.Includes(u => u.Orders, o => o.OrderItems)
|
|
805
|
+
.ToListAsync();
|
|
806
|
+
|
|
807
|
+
// ✅ 只查询需要的字段
|
|
808
|
+
var userDtos = await _db.Queryable<User>()
|
|
809
|
+
.Select(u => new UserDto
|
|
810
|
+
{
|
|
811
|
+
Id = u.Id,
|
|
812
|
+
Name = u.Name,
|
|
813
|
+
OrderCount = SqlFunc.Subqueryable<Order>().Where(o => o.UserId == u.Id).Count()
|
|
814
|
+
})
|
|
815
|
+
.ToListAsync();
|
|
816
|
+
|
|
817
|
+
// ✅ 分页查询
|
|
818
|
+
var (list, total) = await _db.Queryable<User>()
|
|
819
|
+
.Where(u => u.Status == 1)
|
|
820
|
+
.OrderByDescending(u => u.CreateTime)
|
|
821
|
+
.ToPageListAsync(pageIndex, pageSize);
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
### 事务处理
|
|
825
|
+
|
|
826
|
+
```csharp
|
|
827
|
+
// ✅ 使用 UnitOfWork
|
|
828
|
+
public class OrderService : ITransient
|
|
829
|
+
{
|
|
830
|
+
private readonly ISqlSugarRepository<Order> _orderRepo;
|
|
831
|
+
private readonly ISqlSugarRepository<OrderItem> _itemRepo;
|
|
832
|
+
|
|
833
|
+
public async Task CreateOrderAsync(CreateOrderDto dto)
|
|
834
|
+
{
|
|
835
|
+
try
|
|
836
|
+
{
|
|
837
|
+
_orderRepo.Ado.BeginTran();
|
|
838
|
+
|
|
839
|
+
var order = dto.Adapt<Order>();
|
|
840
|
+
var orderId = await _orderRepo.InsertReturnIdentityAsync(order);
|
|
841
|
+
|
|
842
|
+
var items = dto.Items.Select(i => new OrderItem
|
|
843
|
+
{
|
|
844
|
+
OrderId = orderId,
|
|
845
|
+
ProductId = i.ProductId,
|
|
846
|
+
Quantity = i.Quantity
|
|
847
|
+
}).ToList();
|
|
848
|
+
|
|
849
|
+
await _itemRepo.InsertRangeAsync(items);
|
|
850
|
+
|
|
851
|
+
_orderRepo.Ado.CommitTran();
|
|
852
|
+
}
|
|
853
|
+
catch
|
|
854
|
+
{
|
|
855
|
+
_orderRepo.Ado.RollbackTran();
|
|
856
|
+
throw;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// ✅ 使用 Furion 工作单元
|
|
862
|
+
[UnitOfWork]
|
|
863
|
+
public async Task CreateOrderAsync(CreateOrderDto dto)
|
|
864
|
+
{
|
|
865
|
+
// 自动开启事务,方法结束自动提交,异常自动回滚
|
|
866
|
+
var order = dto.Adapt<Order>();
|
|
867
|
+
await _orderRepo.InsertAsync(order);
|
|
868
|
+
await _itemRepo.InsertRangeAsync(dto.Items);
|
|
869
|
+
}
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
### 批量操作
|
|
873
|
+
|
|
874
|
+
```csharp
|
|
875
|
+
// ✅ 批量插入
|
|
876
|
+
await _db.Insertable(users).ExecuteCommandAsync();
|
|
877
|
+
|
|
878
|
+
// ✅ 批量更新
|
|
879
|
+
await _db.Updateable(users).ExecuteCommandAsync();
|
|
880
|
+
|
|
881
|
+
// ✅ 批量删除
|
|
882
|
+
await _db.Deleteable<User>().In(ids).ExecuteCommandAsync();
|
|
883
|
+
|
|
884
|
+
// ✅ 条件更新(只更新指定字段)
|
|
885
|
+
await _db.Updateable<User>()
|
|
886
|
+
.SetColumns(u => u.Status == 0)
|
|
887
|
+
.Where(u => u.ExpireTime < DateTime.Now)
|
|
888
|
+
.ExecuteCommandAsync();
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
## 性能考虑
|
|
892
|
+
|
|
893
|
+
<!-- [注释] 先写正确的代码,再优化性能 -->
|
|
894
|
+
|
|
895
|
+
### 核心原则
|
|
896
|
+
|
|
897
|
+
| 原则 | 说明 |
|
|
898
|
+
|------|------|
|
|
899
|
+
| **先正确后优化** | 先确保功能正确,再考虑性能 |
|
|
900
|
+
| **先测量后优化** | 用 BenchmarkDotNet / dotTrace 定位瓶颈 |
|
|
901
|
+
| **避免过早优化** | 可读性优先,除非有明确的性能需求 |
|
|
902
|
+
|
|
903
|
+
### 避免常见陷阱
|
|
904
|
+
|
|
905
|
+
| 陷阱 | 解决方案 |
|
|
906
|
+
|------|---------|
|
|
907
|
+
| N+1 查询 | 使用 Includes 导航查询 |
|
|
908
|
+
| 循环中拼接字符串 | 使用 `StringBuilder` |
|
|
909
|
+
| 频繁分配临时对象 | 使用 `Span<T>`、`ArrayPool<T>` |
|
|
910
|
+
| 阻塞异步调用 | 使用 async/await |
|
|
911
|
+
| 未使用只读查询 | 只查询不修改时避免跟踪 |
|
|
912
|
+
| 加载过多数据 | 使用分页、投影 |
|
|
913
|
+
|
|
914
|
+
### 字符串处理
|
|
915
|
+
|
|
916
|
+
```csharp
|
|
917
|
+
// ❌ 差:循环拼接字符串
|
|
918
|
+
var result = "";
|
|
919
|
+
foreach (var s in strings)
|
|
920
|
+
{
|
|
921
|
+
result += s; // 每次创建新对象
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// ✅ 好:使用 StringBuilder
|
|
925
|
+
var sb = new StringBuilder(estimatedSize);
|
|
926
|
+
foreach (var s in strings)
|
|
927
|
+
{
|
|
928
|
+
sb.Append(s);
|
|
929
|
+
}
|
|
930
|
+
var result = sb.ToString();
|
|
931
|
+
|
|
932
|
+
// ✅ 好:使用 string.Join
|
|
933
|
+
var result = string.Join(",", strings);
|
|
934
|
+
|
|
935
|
+
// ✅ 好:使用字符串插值(少量拼接)
|
|
936
|
+
var message = $"User {name} created at {DateTime.Now}";
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
### 集合与 LINQ 优化
|
|
940
|
+
|
|
941
|
+
```csharp
|
|
942
|
+
// ✅ 预分配容量
|
|
943
|
+
var list = new List<User>(expectedSize);
|
|
944
|
+
var dict = new Dictionary<long, User>(expectedSize);
|
|
945
|
+
|
|
946
|
+
// ✅ 使用 HashSet 进行包含检查
|
|
947
|
+
var ids = userIds.ToHashSet();
|
|
948
|
+
var filtered = allUsers.Where(u => ids.Contains(u.Id));
|
|
949
|
+
|
|
950
|
+
// ❌ 差:多次枚举 IEnumerable
|
|
951
|
+
var query = GetUsers(); // IEnumerable
|
|
952
|
+
var count = query.Count(); // 第一次枚举
|
|
953
|
+
var first = query.First(); // 第二次枚举
|
|
954
|
+
|
|
955
|
+
// ✅ 好:先具体化
|
|
956
|
+
var users = GetUsers().ToList();
|
|
957
|
+
var count = users.Count;
|
|
958
|
+
var first = users[0];
|
|
959
|
+
```
|
|
960
|
+
|
|
961
|
+
### 性能分析工具
|
|
962
|
+
|
|
963
|
+
```bash
|
|
964
|
+
# 使用 BenchmarkDotNet
|
|
965
|
+
dotnet add package BenchmarkDotNet
|
|
966
|
+
|
|
967
|
+
# 运行基准测试
|
|
968
|
+
dotnet run -c Release
|
|
969
|
+
|
|
970
|
+
# 使用 dotnet-counters 监控
|
|
971
|
+
dotnet counters monitor --process-id <pid>
|
|
972
|
+
|
|
973
|
+
# 使用 dotnet-trace 采集性能数据
|
|
974
|
+
dotnet trace collect --process-id <pid>
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
## 规则溯源要求
|
|
978
|
+
|
|
979
|
+
当回复明确受到本规则约束时,在回复末尾声明:
|
|
980
|
+
|
|
981
|
+
```
|
|
982
|
+
> 📋 本回复遵循规则:`dotnet-style.md` - [具体章节]
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
---
|
|
986
|
+
|
|
987
|
+
## 参考资料
|
|
988
|
+
|
|
989
|
+
- [Microsoft C# Coding Conventions](https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions)
|
|
990
|
+
- [Furion 官方文档](https://furion.baiqian.ltd/)
|
|
991
|
+
- [SqlSugar 官方文档](https://www.donet5.com/Home/Doc)
|