|
| 1 | +# C# Rules |
| 2 | + |
| 3 | +## Design |
| 4 | + |
| 5 | +- Design the change, don't just complete the feature: a working implementation that leaves tech debt is not acceptable. If every viable approach leaves debt, the module itself is due for a redesign — surface that instead of bolting on. |
| 6 | +- Build along the known extension axes; this app already paid for assuming a single game (the `[PerGame]`/game-scope rework when multi-game support arrived). Ask "what varies when the next game/variant arrives" before hardcoding, and keep game-specific state and services game-scoped. |
| 7 | + |
| 8 | +## Language & APIs |
| 9 | + |
| 10 | +- Use the newest C# syntax and BCL APIs the repo's `LangVersion`/TFM allows (`field` keyword, `extension` members, collection expressions, span/`IFormatProvider` overloads) — never an older equivalent out of habit; the newer API is usually the better-optimized one. |
| 11 | +- When several APIs solve the same problem, pick the best-performing one for the concrete situation (allocations, lookup cost, parse overloads) — deliberately, not first-that-works. |
| 12 | + |
| 13 | +## Naming & comments |
| 14 | + |
| 15 | +- Method and variable names must be self-explanatory; if a name needs a comment to be understood, rename it (e.g. `PlayingFolderName`, not `CurrentlyPlaying` + a comment). |
| 16 | +- When a name is hard to settle, mirror an existing name in this project first; with no precedent, follow the general .NET/community convention for that concept (how the BCL or well-known libraries name it). |
| 17 | +- No narration comments: never comment what the code does, where it came from, or why a change is correct. |
| 18 | +- The only acceptable comment is a single short line stating a constraint the code cannot show (threading requirements, external library quirks). No multi-line essays. |
| 19 | +- No XML doc comments on members whose name already says everything. |
| 20 | + |
| 21 | +## Async |
| 22 | + |
| 23 | +- Never wrap an async call chain in `Task.Run`. If the chain has real awaits (file I/O, network), it does not block the caller. `Task.Run` is only justified to push genuinely synchronous blocking work (e.g. synchronous audio decode) off the UI thread. |
| 24 | +- `ConfigureAwait(false)` on every await outside UI-bound continuations; use `Dispatcher.UIThread` explicitly when touching UI-bound state afterwards. |
| 25 | + |
| 26 | +## Code organization |
| 27 | + |
| 28 | +- Where a type belongs is defined by the project map in CLAUDE.md. |
| 29 | +- Values derivable from a model are computed properties on the model itself (see `ChartDto.SizeDisplay`, `BpmDisplay`, `DifficultyBadges`), not static helper classes or converter wrappers. |
| 30 | +- Large types split into `Name.Topic.cs` partial files. |
| 31 | +- Service layout: the suffix-less `<Service>.cs` holds only the public interface implementation. Everything else lives in partials — `<Service>.Private.cs` when the helpers are few, `<Service>.<Topic>.cs` (`.Core`, `.Import`, ...) when there is enough to split by topic. |
| 32 | +- Private helper types are nested inside the owning partial class file, not separate top-level types. |
| 33 | + |
| 34 | +## Services & notifications |
| 35 | + |
| 36 | +- Toast notifications live in core services, not viewmodels. Worker methods return results; the public method notifies per granularity: single operation = per-item toast, bulk operation = one summary toast. Never add a notification-suppress flag. |
| 37 | +- Bulk methods return a count so an explicit UI action can toast "nothing to do" (silent no-op stays correct for automatic startup paths). |
| 38 | +- No redundant wrapper services or pass-through methods: put the method on the existing domain service and make the real method public. |
| 39 | +- File/IO primitives go through `IFileSystemService` (`Try*` pattern); never raw `Directory`/`File` calls with ad-hoc try/catch in services. |
| 40 | + |
| 41 | +## Localization |
| 42 | + |
| 43 | +- During development add the neutral `.resx` plus `zh-Hans`/`zh-Hant` only — those are the locales the user can read and test with, and keys may still churn while the feature settles. The remaining locales get batch-translated once the feature is final. |
| 44 | +- Pick the table by where the text is shown, not by who assigns it: `XAML` = presentation-surface text (labels, titles, descriptions, button captions, and inline status/progress shown inside a page/panel — even when set from a viewmodel or core service, e.g. `MelonLoader_State_Downloading`, `Setup_Step_Failed`, `Setup_Progress_*`); `Interaction` = transient interaction surfaces (message boxes, toast notifications, OS file/folder pickers). So "code-side messages in `Interaction`" below means dialog/notification text, not every code-set string; a core service reads an `XAML` value via `XAML.Key` (Core imports only `static Interaction`, so add `global using Euterpe.Localization;` there). |
| 45 | +- Keys are `Name_Action_State` segments, scoped by where the text belongs: |
| 46 | + - Page/panel-scoped view text: `<PanelName>_<Element>` (`ChartManage_SortBy`, `Setting_Title_Language`). |
| 47 | + - Self-identifying concepts, especially enum members: `<TypeName>_<Member>` (`ChartDifficulty_Easy`, `ChartSortField_MapCount`, `ModFilterType_All`) — never borrow another feature's key because the value happens to match today. |
| 48 | + - Code-side messages in `Interaction`: `<Kind>_Content_<Domain>_<Action>_<State>` (`Notification_Content_Chart_UpdateAll_Success`, `MessageBox_Content_SetPathEnvironment_Windows`). |
| 49 | +- Keep `.resx` data entries alphabetically ordered — merge new keys into place, never append at the end. |
| 50 | + |
| 51 | +## Misc |
| 52 | + |
| 53 | +- Deduplicate shared one-liners into a single home (e.g. `ChartFiles.MapName`), even tiny ones. |
| 54 | +- Before writing anything by hand, check the packages already referenced (`Directory.Packages.props`) for an existing facility — especially the easily-forgotten ones: CommunityToolkit.Mvvm's `[NotifyPropertyChangedFor]` for dependent properties instead of manually raising `OnPropertyChanged`, R3 `Observable`s for event wiring/composition instead of manual event handlers. |
| 55 | +- Prefer an existing library over hand-rolling, but verify its actual behavior (decompile if needed) before adopting or rejecting it. |
| 56 | +- Logging via ZLogger interpolation (`Logger.ZLogInformation($"...")`). |
| 57 | +- DI: `public required T Name { get; init; }` properties inside `#region Injections`. |
| 58 | +- `.cs` files end with exactly one trailing newline (`insert_final_newline` applies to `[*.cs]` only). |
0 commit comments