@dynokostya/just-works 1.0.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/.claude/agents/csharp-code-writer.md +32 -0
- package/.claude/agents/diagrammer.md +49 -0
- package/.claude/agents/frontend-code-writer.md +36 -0
- package/.claude/agents/prompt-writer.md +38 -0
- package/.claude/agents/python-code-writer.md +32 -0
- package/.claude/agents/swift-code-writer.md +32 -0
- package/.claude/agents/typescript-code-writer.md +32 -0
- package/.claude/commands/git-sync.md +96 -0
- package/.claude/commands/project-docs.md +287 -0
- package/.claude/settings.json +112 -0
- package/.claude/settings.json.default +15 -0
- package/.claude/skills/csharp-coding/SKILL.md +368 -0
- package/.claude/skills/ddd-architecture-python/SKILL.md +288 -0
- package/.claude/skills/feature-driven-architecture-python/SKILL.md +302 -0
- package/.claude/skills/gemini-3-prompting/SKILL.md +483 -0
- package/.claude/skills/gpt-5-2-prompting/SKILL.md +295 -0
- package/.claude/skills/opus-4-6-prompting/SKILL.md +315 -0
- package/.claude/skills/plantuml-diagramming/SKILL.md +758 -0
- package/.claude/skills/python-coding/SKILL.md +293 -0
- package/.claude/skills/react-coding/SKILL.md +264 -0
- package/.claude/skills/rest-api/SKILL.md +421 -0
- package/.claude/skills/shadcn-ui-coding/SKILL.md +454 -0
- package/.claude/skills/swift-coding/SKILL.md +401 -0
- package/.claude/skills/tailwind-css-coding/SKILL.md +268 -0
- package/.claude/skills/typescript-coding/SKILL.md +464 -0
- package/.claude/statusline-command.sh +34 -0
- package/.codex/prompts/plan-reviewer.md +162 -0
- package/.codex/prompts/project-docs.md +287 -0
- package/.codex/skills/ddd-architecture-python/SKILL.md +288 -0
- package/.codex/skills/feature-driven-architecture-python/SKILL.md +302 -0
- package/.codex/skills/gemini-3-prompting/SKILL.md +483 -0
- package/.codex/skills/gpt-5-2-prompting/SKILL.md +295 -0
- package/.codex/skills/opus-4-6-prompting/SKILL.md +315 -0
- package/.codex/skills/plantuml-diagramming/SKILL.md +758 -0
- package/.codex/skills/python-coding/SKILL.md +293 -0
- package/.codex/skills/react-coding/SKILL.md +264 -0
- package/.codex/skills/rest-api/SKILL.md +421 -0
- package/.codex/skills/shadcn-ui-coding/SKILL.md +454 -0
- package/.codex/skills/tailwind-css-coding/SKILL.md +268 -0
- package/.codex/skills/typescript-coding/SKILL.md +464 -0
- package/AGENTS.md +57 -0
- package/CLAUDE.md +98 -0
- package/LICENSE +201 -0
- package/README.md +114 -0
- package/bin/cli.mjs +291 -0
- package/package.json +39 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(cd:*)",
|
|
5
|
+
"Bash(find:*)",
|
|
6
|
+
"Bash(ls:*)",
|
|
7
|
+
"Bash(grep:*)",
|
|
8
|
+
"Bash(tree:*)",
|
|
9
|
+
"Bash(wc:*)",
|
|
10
|
+
"Bash(file:*)",
|
|
11
|
+
"Bash(du:*)",
|
|
12
|
+
"Bash(which:*)",
|
|
13
|
+
"Bash(diff:*)",
|
|
14
|
+
"Bash(pwd:*)",
|
|
15
|
+
"Bash(head:*)",
|
|
16
|
+
"Bash(tail:*)",
|
|
17
|
+
"Bash(sort:*)",
|
|
18
|
+
"Bash(uniq:*)",
|
|
19
|
+
"Bash(echo:*)"
|
|
20
|
+
],
|
|
21
|
+
"deny": [
|
|
22
|
+
"Read(**/.env)",
|
|
23
|
+
"Read(**/.env*)",
|
|
24
|
+
"Read(**/*.pem)",
|
|
25
|
+
"Read(**/*.key)",
|
|
26
|
+
"Read(**/*.p12)",
|
|
27
|
+
"Read(**/*.pfx)",
|
|
28
|
+
"Read(**/*.jks)",
|
|
29
|
+
"Read(**/.netrc)",
|
|
30
|
+
"Read(**/.pgpass)",
|
|
31
|
+
"Read(**/.aws/*)",
|
|
32
|
+
"Read(**/.gcp/*)",
|
|
33
|
+
"Read(**/service-account*.json)",
|
|
34
|
+
"Read(**/.azure/*)",
|
|
35
|
+
"Read(**/terraform.tfvars)",
|
|
36
|
+
"Read(**/*.tfstate)",
|
|
37
|
+
"Read(**/*.tfstate.*)",
|
|
38
|
+
"Read(**/.ssh/*)",
|
|
39
|
+
"Read(**/.gnupg/*)",
|
|
40
|
+
"Read(**/*.sqlite)",
|
|
41
|
+
"Read(**/*.db)",
|
|
42
|
+
"Read(**/.htpasswd)"
|
|
43
|
+
],
|
|
44
|
+
"defaultMode": "bypassPermissions"
|
|
45
|
+
},
|
|
46
|
+
"model": "opus[1m]",
|
|
47
|
+
"enableAllProjectMcpServers": true,
|
|
48
|
+
"enabledMcpjsonServers": [
|
|
49
|
+
"Context7"
|
|
50
|
+
],
|
|
51
|
+
"cleanupPeriodDays": 180,
|
|
52
|
+
"maxTerminalOutputChars": 150000,
|
|
53
|
+
"maxFileReadTokens": 30000,
|
|
54
|
+
"autoCompactPercentageOverride": 80,
|
|
55
|
+
"autoMemoryEnabled": false,
|
|
56
|
+
"skipDangerousModePermissionPrompt": true,
|
|
57
|
+
"plansDirectory": "./.claude/plans",
|
|
58
|
+
"outputStyle": "default",
|
|
59
|
+
"spinnerTipsEnabled": true,
|
|
60
|
+
"attribution": {
|
|
61
|
+
"commit": "",
|
|
62
|
+
"pr": ""
|
|
63
|
+
},
|
|
64
|
+
"env": {
|
|
65
|
+
"CLAUDE_CODE_SUBAGENT_MODEL": "opus",
|
|
66
|
+
"CLAUDE_CODE_EFFORT_LEVEL": "max",
|
|
67
|
+
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
|
|
68
|
+
"DISABLE_TELEMETRY": "1",
|
|
69
|
+
"DISABLE_ERROR_REPORTING": "1",
|
|
70
|
+
"DISABLE_FEEDBACK_COMMAND": "1",
|
|
71
|
+
"CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY": "1",
|
|
72
|
+
"CLAUDE_CODE_DISABLE_FAST_MODE": "1",
|
|
73
|
+
"BASH_MAX_OUTPUT_LENGTH": "150000"
|
|
74
|
+
},
|
|
75
|
+
"hooks": {
|
|
76
|
+
"PreToolUse": [
|
|
77
|
+
{
|
|
78
|
+
"matcher": "Read",
|
|
79
|
+
"hooks": [
|
|
80
|
+
{
|
|
81
|
+
"type": "command",
|
|
82
|
+
"command": "FILE=$(echo '$CLAUDE_TOOL_INPUT' | jq -r '.file_path') && [ -f \"$FILE\" ] && LINES=$(wc -l < \"$FILE\") && [ \"$LINES\" -gt 500 ] && echo 'File is too large, do not read it fully. Use offset and limit parameters to read specific sections.' >&2 && exit 2 || exit 0"
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"matcher": "Bash",
|
|
88
|
+
"hooks": [
|
|
89
|
+
{
|
|
90
|
+
"type": "command",
|
|
91
|
+
"command": "/Users/dynokostya/.claude/hooks/rtk-rewrite.sh"
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
],
|
|
96
|
+
"Notification": [
|
|
97
|
+
{
|
|
98
|
+
"matcher": "*",
|
|
99
|
+
"hooks": [
|
|
100
|
+
{
|
|
101
|
+
"type": "command",
|
|
102
|
+
"command": "afplay /System/Library/Sounds/Glass.aiff"
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
"statusLine": {
|
|
109
|
+
"type": "command",
|
|
110
|
+
"command": "bash ~/.claude/statusline-command.sh"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [],
|
|
4
|
+
"deny": []
|
|
5
|
+
},
|
|
6
|
+
"hooks": {},
|
|
7
|
+
"outputStyle": "default",
|
|
8
|
+
"spinnerTipsEnabled": true,
|
|
9
|
+
"plansDirectory": "./.claude/plans",
|
|
10
|
+
"statusLine": {
|
|
11
|
+
"type": "command",
|
|
12
|
+
"command": "bash ~/.claude/statusline-command.sh"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: csharp-coding
|
|
3
|
+
description: Apply when writing or editing C# (.cs) files. Behavioral corrections for error handling, resource management, async patterns, data modeling, type safety, nullability, security defaults, and common antipatterns. Project conventions always override these defaults.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# C# Coding
|
|
7
|
+
|
|
8
|
+
Match the project's existing conventions. When uncertain, read 2-3 existing files to infer the local style. Check `.csproj` for target framework, C# language version, nullable settings, and analyzer config. These defaults apply only when the project has no established convention.
|
|
9
|
+
|
|
10
|
+
## Never rules
|
|
11
|
+
|
|
12
|
+
These are unconditional. They prevent bugs and vulnerabilities regardless of project style.
|
|
13
|
+
|
|
14
|
+
- **Never `catch { }` or `catch (Exception) { }` without rethrow or logging.** Broad catches silently swallow bugs — a NullReferenceException from a typo looks the same as a transient network failure. Catch specific exception types. If you must catch broadly at a boundary, always log and rethrow or convert to a meaningful error.
|
|
15
|
+
- **Never `throw ex`** — use `throw` or `throw new XException("msg", ex)`. `throw ex` resets the stack trace, destroying the origin of the error. You'll spend hours debugging something the original stack trace would have shown instantly.
|
|
16
|
+
- **Never `DateTime.Now`** — use `DateTime.UtcNow` or `DateTimeOffset.UtcNow`. `DateTime.Now` produces local time that varies by server timezone and breaks across DST transitions. `DateTimeOffset` is preferred when the timezone context matters.
|
|
17
|
+
- **Never `async void`** — except for event handlers. `async void` methods cannot be awaited, their exceptions crash the process unobserved, and they break structured error handling. Use `async Task` instead.
|
|
18
|
+
- **Never `.Result`, `.Wait()`, or `.GetAwaiter().GetResult()` on tasks** — these block the calling thread and cause deadlocks in ASP.NET Core and UI contexts. Use `await` instead. If you're in a sync context that genuinely cannot be made async, use `Task.Run(() => AsyncMethod()).GetAwaiter().GetResult()` as a last resort with a comment explaining why.
|
|
19
|
+
- **Never `Random` for security** — `Random` is not cryptographically secure and is predictable. Use `RandomNumberGenerator.GetBytes()`, `RandomNumberGenerator.GetInt32()`, or `Convert.ToHexString(RandomNumberGenerator.GetBytes(n))` for tokens, keys, session IDs.
|
|
20
|
+
- **Never string interpolation or concatenation in SQL** — use parameterized queries only. `$"SELECT * FROM users WHERE id = {uid}"` is always a SQL injection vulnerability. Use `@param` placeholders with `SqlCommand.Parameters` or your ORM's parameterization.
|
|
21
|
+
- **Never `GC.Collect()`** — the GC is self-tuning. Forcing collection hurts performance in nearly all cases by promoting short-lived objects to higher generations. If you think you need it, you have a design problem to fix instead.
|
|
22
|
+
- **Never mutable static fields** — statics are shared across all threads. Mutable statics cause race conditions that are extremely hard to reproduce and debug. Use `IOptions<T>`, dependency injection, or `ConcurrentDictionary` if shared state is genuinely needed.
|
|
23
|
+
- **Never `+` string concatenation in loops** — use `StringBuilder`. Strings are immutable in C#, so each `+` allocates a new string and copies all previous content — O(n^2) at scale. At 10k iterations this turns milliseconds into seconds.
|
|
24
|
+
- **Never `dynamic` for typed data** — `dynamic` bypasses compile-time type checking, turns type errors into runtime exceptions, and kills IntelliSense. Use generics, interfaces, or pattern matching instead. Reserve `dynamic` for COM interop or truly dynamic scenarios.
|
|
25
|
+
- **Never `Thread.Sleep()` in async code** — use `await Task.Delay()`. `Thread.Sleep` blocks the thread pool thread, starving other async operations. In ASP.NET Core this can exhaust the thread pool under load.
|
|
26
|
+
|
|
27
|
+
## Error handling
|
|
28
|
+
|
|
29
|
+
Use `throw` (not `throw ex`) when re-throwing to preserve the original stack trace. Wrap with inner exception when converting exception types at boundaries:
|
|
30
|
+
|
|
31
|
+
```csharp
|
|
32
|
+
try
|
|
33
|
+
{
|
|
34
|
+
var result = await httpClient.GetAsync(url, cancellationToken);
|
|
35
|
+
result.EnsureSuccessStatusCode();
|
|
36
|
+
}
|
|
37
|
+
catch (HttpRequestException ex)
|
|
38
|
+
{
|
|
39
|
+
throw new ServiceUnavailableException($"Failed to reach {url}", ex);
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Use exception filters (`when`) to catch conditionally without unwinding the stack:
|
|
44
|
+
|
|
45
|
+
```csharp
|
|
46
|
+
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
|
|
47
|
+
{
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
catch (HttpRequestException ex) when (ex.StatusCode >= HttpStatusCode.InternalServerError)
|
|
51
|
+
{
|
|
52
|
+
logger.LogWarning(ex, "Transient server error from {Url}", url);
|
|
53
|
+
throw;
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Create custom exception types when callers need to distinguish failure modes:
|
|
58
|
+
|
|
59
|
+
```csharp
|
|
60
|
+
public class AppException : Exception
|
|
61
|
+
{
|
|
62
|
+
public AppException(string message) : base(message) { }
|
|
63
|
+
public AppException(string message, Exception inner) : base(message, inner) { }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public class NotFoundException : AppException
|
|
67
|
+
{
|
|
68
|
+
public string Resource { get; }
|
|
69
|
+
public object Id { get; }
|
|
70
|
+
|
|
71
|
+
public NotFoundException(string resource, object id)
|
|
72
|
+
: base($"{resource} not found: {id}")
|
|
73
|
+
{
|
|
74
|
+
Resource = resource;
|
|
75
|
+
Id = id;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Resource cleanup
|
|
81
|
+
|
|
82
|
+
Use `using` declarations for anything that implements `IDisposable` or `IAsyncDisposable`. Never instantiate `HttpClient`, database connections, streams, or similar without `using` or explicit `finally` cleanup.
|
|
83
|
+
|
|
84
|
+
```csharp
|
|
85
|
+
// C# 8+ using declaration — disposed at end of scope
|
|
86
|
+
using var stream = File.OpenRead(path);
|
|
87
|
+
using var reader = new StreamReader(stream);
|
|
88
|
+
var content = await reader.ReadToEndAsync(cancellationToken);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
For async disposables:
|
|
92
|
+
|
|
93
|
+
```csharp
|
|
94
|
+
await using var connection = new SqlConnection(connectionString);
|
|
95
|
+
await connection.OpenAsync(cancellationToken);
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
For multiple resources or complex lifecycles, use explicit `try/finally`:
|
|
99
|
+
|
|
100
|
+
```csharp
|
|
101
|
+
var connection = new SqlConnection(connectionString);
|
|
102
|
+
try
|
|
103
|
+
{
|
|
104
|
+
await connection.OpenAsync(cancellationToken);
|
|
105
|
+
// work
|
|
106
|
+
}
|
|
107
|
+
finally
|
|
108
|
+
{
|
|
109
|
+
await connection.DisposeAsync();
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
When implementing `IDisposable`, follow the pattern only if you hold unmanaged resources or own other disposables. Don't implement `IDisposable` on classes that don't own anything disposable — it adds ceremony for no benefit.
|
|
114
|
+
|
|
115
|
+
## Async patterns
|
|
116
|
+
|
|
117
|
+
**Always pass `CancellationToken` through the call chain.** Accept it as the last parameter and forward it to every async API. Omitting it means your code cannot be cancelled, leading to wasted resources and hung requests.
|
|
118
|
+
|
|
119
|
+
```csharp
|
|
120
|
+
public async Task<User> GetUserAsync(int id, CancellationToken cancellationToken = default)
|
|
121
|
+
{
|
|
122
|
+
var response = await _httpClient.GetAsync($"/users/{id}", cancellationToken);
|
|
123
|
+
response.EnsureSuccessStatusCode();
|
|
124
|
+
return await response.Content.ReadFromJsonAsync<User>(cancellationToken: cancellationToken)
|
|
125
|
+
?? throw new NotFoundException("User", id);
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Prefer `Task` over `ValueTask`** unless profiling shows allocation pressure from a hot path that frequently completes synchronously. `ValueTask` has restrictions (can only be awaited once, no concurrent awaits) that make it error-prone as a default choice.
|
|
130
|
+
|
|
131
|
+
**Use `ConfigureAwait(false)` in library code** (NuGet packages, shared class libraries) to avoid deadlocks in non-ASP.NET Core consumers. In ASP.NET Core application code, it's unnecessary — the default `SynchronizationContext` is null.
|
|
132
|
+
|
|
133
|
+
**Use `Task.WhenAll` for independent concurrent work:**
|
|
134
|
+
|
|
135
|
+
```csharp
|
|
136
|
+
var usersTask = GetUsersAsync(cancellationToken);
|
|
137
|
+
var ordersTask = GetOrdersAsync(cancellationToken);
|
|
138
|
+
|
|
139
|
+
await Task.WhenAll(usersTask, ordersTask);
|
|
140
|
+
|
|
141
|
+
var users = await usersTask;
|
|
142
|
+
var orders = await ordersTask;
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Never use `async` on a method that just returns another task.** Remove the state machine overhead:
|
|
146
|
+
|
|
147
|
+
```csharp
|
|
148
|
+
// Wrong — unnecessary async state machine
|
|
149
|
+
public async Task<User> GetUserAsync(int id, CancellationToken ct)
|
|
150
|
+
=> await _repository.GetByIdAsync(id, ct);
|
|
151
|
+
|
|
152
|
+
// Correct — direct passthrough
|
|
153
|
+
public Task<User> GetUserAsync(int id, CancellationToken ct)
|
|
154
|
+
=> _repository.GetByIdAsync(id, ct);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Exception: keep `async`/`await` when you need `using`, `try/catch`, or when the method does work before/after the awaited call.
|
|
158
|
+
|
|
159
|
+
## Nullability
|
|
160
|
+
|
|
161
|
+
Enable nullable reference types (`<Nullable>enable</Nullable>` in .csproj). Use the compiler's flow analysis — don't add redundant null checks where the type system already guarantees non-null.
|
|
162
|
+
|
|
163
|
+
**Use `??` for defaults and `?.` for optional chains:**
|
|
164
|
+
|
|
165
|
+
```csharp
|
|
166
|
+
var name = user.DisplayName ?? user.Email ?? "Anonymous";
|
|
167
|
+
var city = user.Address?.City;
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Use `??=` for lazy initialization:**
|
|
171
|
+
|
|
172
|
+
```csharp
|
|
173
|
+
_cache ??= new Dictionary<string, object>();
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Guard clause pattern for required non-null arguments:**
|
|
177
|
+
|
|
178
|
+
```csharp
|
|
179
|
+
public UserService(IUserRepository repository, ILogger<UserService> logger)
|
|
180
|
+
{
|
|
181
|
+
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
|
182
|
+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
With C# 11+ and nullable enabled, prefer `required` keyword or primary constructors over null checks for constructor injection when the DI container guarantees non-null.
|
|
187
|
+
|
|
188
|
+
## Pattern matching
|
|
189
|
+
|
|
190
|
+
Use switch expressions for exhaustive branching. Clearer than if/elif chains for type checks, property destructuring, and value mapping.
|
|
191
|
+
|
|
192
|
+
```csharp
|
|
193
|
+
var message = result switch
|
|
194
|
+
{
|
|
195
|
+
{ IsSuccess: true, Value: var v } => $"Got {v}",
|
|
196
|
+
{ Error: NotFoundError e } => $"{e.Resource} not found",
|
|
197
|
+
{ Error: ValidationError e } => $"Invalid: {string.Join(", ", e.Errors)}",
|
|
198
|
+
_ => "Unknown error"
|
|
199
|
+
};
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Property patterns for conditional logic:
|
|
203
|
+
|
|
204
|
+
```csharp
|
|
205
|
+
if (order is { Status: OrderStatus.Shipped, TrackingNumber: not null } shipped)
|
|
206
|
+
{
|
|
207
|
+
NotifyCustomer(shipped.TrackingNumber);
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Relational and logical patterns:
|
|
212
|
+
|
|
213
|
+
```csharp
|
|
214
|
+
var tier = score switch
|
|
215
|
+
{
|
|
216
|
+
>= 90 => "Gold",
|
|
217
|
+
>= 70 => "Silver",
|
|
218
|
+
>= 50 => "Bronze",
|
|
219
|
+
_ => "None"
|
|
220
|
+
};
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Data modeling
|
|
224
|
+
|
|
225
|
+
| Use Case | Choice | Reason |
|
|
226
|
+
|----------|--------|--------|
|
|
227
|
+
| API request/response DTOs | record | Immutable, value equality, concise syntax |
|
|
228
|
+
| Configuration | record or POCO + IOptions<T> | Framework support, binding |
|
|
229
|
+
| Domain entities with identity | class | Reference equality, mutable state |
|
|
230
|
+
| Simple value objects | readonly record struct | Stack-allocated, no GC pressure, value semantics |
|
|
231
|
+
|
|
232
|
+
Records for DTOs:
|
|
233
|
+
|
|
234
|
+
```csharp
|
|
235
|
+
public sealed record CreateUserRequest(string Email, string Name);
|
|
236
|
+
|
|
237
|
+
public sealed record UserResponse(int Id, string Email, string Name);
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Primary constructors (C# 12) for service classes:
|
|
241
|
+
|
|
242
|
+
```csharp
|
|
243
|
+
public sealed class UserService(IUserRepository repository, ILogger<UserService> logger)
|
|
244
|
+
{
|
|
245
|
+
public async Task<User> GetByIdAsync(int id, CancellationToken ct)
|
|
246
|
+
{
|
|
247
|
+
logger.LogDebug("Fetching user {UserId}", id);
|
|
248
|
+
return await repository.GetByIdAsync(id, ct)
|
|
249
|
+
?? throw new NotFoundException("User", id);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Use `init` properties when you need object initializer syntax with immutability:
|
|
255
|
+
|
|
256
|
+
```csharp
|
|
257
|
+
public sealed class PaginationOptions
|
|
258
|
+
{
|
|
259
|
+
public int Page { get; init; } = 1;
|
|
260
|
+
public int PageSize { get; init; } = 20;
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Dependency injection
|
|
265
|
+
|
|
266
|
+
Constructor injection. Dependencies are explicit and testable.
|
|
267
|
+
|
|
268
|
+
```csharp
|
|
269
|
+
public sealed class OrderService(
|
|
270
|
+
IOrderRepository repository,
|
|
271
|
+
IPaymentClient payments,
|
|
272
|
+
ILogger<OrderService> logger)
|
|
273
|
+
{
|
|
274
|
+
// ...
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**Service lifetimes:**
|
|
279
|
+
- **Transient** — lightweight, stateless services. New instance per injection.
|
|
280
|
+
- **Scoped** — per-request state (DbContext, unit of work, caller context). One instance per HTTP request.
|
|
281
|
+
- **Singleton** — thread-safe shared state (caches, configuration, HTTP client factories). One instance for app lifetime.
|
|
282
|
+
|
|
283
|
+
Match the project's DI container (Microsoft.Extensions.DependencyInjection, Autofac, etc.). Don't mix container-specific APIs across the codebase.
|
|
284
|
+
|
|
285
|
+
**Register by interface, not concrete type:**
|
|
286
|
+
|
|
287
|
+
```csharp
|
|
288
|
+
services.AddScoped<IUserRepository, UserRepository>();
|
|
289
|
+
services.AddSingleton<ICacheService, RedisCacheService>();
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Enums
|
|
293
|
+
|
|
294
|
+
Use enums for known fixed value sets. Don't use raw strings or magic numbers.
|
|
295
|
+
|
|
296
|
+
```csharp
|
|
297
|
+
public enum OrderStatus
|
|
298
|
+
{
|
|
299
|
+
Pending,
|
|
300
|
+
Confirmed,
|
|
301
|
+
Shipped,
|
|
302
|
+
Delivered,
|
|
303
|
+
Cancelled
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
For enums that need string serialization (APIs, databases), configure the JSON serializer to use string names rather than integer values. Don't scatter `ToString()` / `Enum.Parse()` calls through business logic.
|
|
308
|
+
|
|
309
|
+
## Imports
|
|
310
|
+
|
|
311
|
+
Use `global using` directives (C# 10+) in a single `GlobalUsings.cs` file for project-wide imports. Don't duplicate common usings across every file. Match existing project convention — if there's no `GlobalUsings.cs`, use regular per-file usings.
|
|
312
|
+
|
|
313
|
+
Order: `System.*`, third-party, project namespaces. Let the IDE/formatter sort them.
|
|
314
|
+
|
|
315
|
+
## Logging
|
|
316
|
+
|
|
317
|
+
Use `ILogger<T>` with structured logging. Use message templates with named placeholders — never string interpolation:
|
|
318
|
+
|
|
319
|
+
```csharp
|
|
320
|
+
// Wrong — interpolation defeats structured logging
|
|
321
|
+
logger.LogInformation($"User {userId} created order {orderId}");
|
|
322
|
+
|
|
323
|
+
// Correct — structured, searchable, parameterized
|
|
324
|
+
logger.LogInformation("User {UserId} created order {OrderId}", userId, orderId);
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
For hot paths, use `LoggerMessage.Define` to avoid allocation:
|
|
328
|
+
|
|
329
|
+
```csharp
|
|
330
|
+
private static readonly Action<ILogger, int, Exception?> LogUserCreated =
|
|
331
|
+
LoggerMessage.Define<int>(LogLevel.Information, new EventId(1), "User {UserId} created");
|
|
332
|
+
|
|
333
|
+
// Usage
|
|
334
|
+
LogUserCreated(logger, user.Id, null);
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Or with .NET 6+ source generators:
|
|
338
|
+
|
|
339
|
+
```csharp
|
|
340
|
+
[LoggerMessage(Level = LogLevel.Information, Message = "User {UserId} created")]
|
|
341
|
+
private static partial void LogUserCreated(ILogger logger, int userId);
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
## Testing
|
|
345
|
+
|
|
346
|
+
Match the project's test runner (xUnit, NUnit, MSTest) and mocking library (Moq, NSubstitute, FakeItEasy).
|
|
347
|
+
|
|
348
|
+
When to mock: external HTTP APIs, databases, third-party services, and anything with side effects or costs. When to use real instances: pure logic, in-memory implementations, value objects.
|
|
349
|
+
|
|
350
|
+
Test behavior, not implementation — test what a method returns or what side effects it produces, not how it internally works.
|
|
351
|
+
|
|
352
|
+
```csharp
|
|
353
|
+
[Test]
|
|
354
|
+
public async Task GetUser_WhenExists_ReturnsUser()
|
|
355
|
+
{
|
|
356
|
+
// Arrange
|
|
357
|
+
var repository = Substitute.For<IUserRepository>();
|
|
358
|
+
repository.GetByIdAsync(42, Arg.Any<CancellationToken>())
|
|
359
|
+
.Returns(new User { Id = 42, Name = "Alice" });
|
|
360
|
+
var service = new UserService(repository, NullLogger<UserService>.Instance);
|
|
361
|
+
|
|
362
|
+
// Act
|
|
363
|
+
var result = await service.GetByIdAsync(42, CancellationToken.None);
|
|
364
|
+
|
|
365
|
+
// Assert
|
|
366
|
+
Assert.That(result.Name, Is.EqualTo("Alice"));
|
|
367
|
+
}
|
|
368
|
+
```
|