neon-sprawl/.cursor/rules/csharp-style.md

104 lines
6.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

---
description: C# naming, layout, and primary constructors; Microsoft conventions and .NET idioms.
globs: "**/*.cs"
alwaysApply: true
---
# C# style (Neon Sprawl)
Follow Microsofts **[C# Coding Conventions](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions)** and **[C# identifier rules](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/identifier-names)**. Prefer clarity and consistency with existing server code.
## Naming
- **Types** (classes, structs, records, interfaces, enums, delegates): `PascalCase`.
- **Interfaces:** prefix with `I` (e.g. `IPlayerSession`).
- **Methods, properties, events, public fields:** `PascalCase`.
- **Parameters, local variables:** `camelCase`.
- **Private instance fields:** `camelCase` (no leading underscore), unless an existing file consistently does otherwise—then match the file. If a parameter or local shadows a field, use `this.` or rename for clarity.
- **Static fields:** `camelCase` for private/internal static fields; `PascalCase` for `public static` members (including `readonly`/constants) per Microsoft guidance; stay consistent within a project.
- **Async methods:** suffix with `Async` (e.g. `LoadProfileAsync`).
## Primary constructors
- Prefer **primary constructor** syntax for classes, structs, and records when it fits: dependency injection, `IClassFixture<>` test classes, small services, and any type that mainly captures parameters into fields or base calls.
- **Skip** primary constructors when they hurt clarity: heavy logic in the body that belongs in a conventional constructor, `this` references before the implicit constructor runs in odd ways, or a file already uses a consistent legacy style—then match the file.
```csharp
// Prefer
public sealed class OrderService(IOrderStore store, ILogger<OrderService> log)
{
public Task<Order?> GetAsync(Guid id) => store.FindAsync(id);
}
// Avoid when you would only reassign into mutable fields with non-trivial validation—use an explicit constructor instead.
```
## Layout and syntax
- **Braces:** opening brace `{` on a **new line** for types and members (Allman-style), per common Microsoft examples.
- **Braces for every block:** never omit `{ }` on `if`, `else`, `for`, `foreach`, `while`, `do`, or `using` when the language allows a single statement without braces—**always** use a braced block, even for one line. This avoids accidental logic changes when editing. Expression-bodied members and expression lambdas (`x => x.Id`) stay valid when the whole body is a single expression.
```csharp
// Prefer
if (string.IsNullOrEmpty(key))
{
return false;
}
// Avoid
if (string.IsNullOrEmpty(key))
return false;
```
- **Indentation:** **4 spaces** per level; no tabs unless the file already uses tabs—never mix.
- **`var`:** use when the type is obvious from the right-hand side; use explicit types when it improves readability or for literals where the type matters.
- **File-scoped namespaces** (`namespace X;`) for new single-namespace files when the SDK/version allows.
- **Pattern matching / nullability:** prefer modern C# features where they simplify code; honor **nullable reference type** annotations when the project enables them.
## Members
- Prefer **expression-bodied** members only when they stay one clear idea.
- **LINQ:** favor readability over micro-chains; break complex queries across lines.
- **Exception handling:** catch specific exceptions; avoid empty `catch`; log or rethrow with context when appropriate.
## `Program.cs` and minimal APIs
- Top-level statements and minimal APIs are fine for small apps; extract registration/build logic into extension methods or dedicated types when the file grows.
## Documentation
- Use **`///` XML doc comments** on public APIs (types and members) when behavior or contracts are not obvious.
## Test project layout (`*.Tests`)
- **Mirror the server project:** place test types under the same relative path as the production code they exercise (e.g. `NeonSprawl.Server/Game/PositionState/PositionStateApi.cs``NeonSprawl.Server.Tests/Game/PositionState/PositionStateApiTests.cs`).
- Use a namespace that matches the folder tree under the test assembly root, e.g. `NeonSprawl.Server.Tests.Game.PositionState` for files in `Game/PositionState/`.
## Test method naming convention
- Use **`MethodName_ShouldExpectedOutcome_WhenScenario`** (three segments, **`PascalCase`** inside each segment, separated by **underscores**).
- **`MethodName`:** the behavior or entry under test (SUT method, HTTP operation, or short feature verb)—e.g. `GetPosition`, `PostMove`, `TryApplyMoveTarget`.
- **`ShouldExpectedOutcome`:** the outcome the test proves—e.g. `ShouldReturnNotFound`, `ShouldPersistTargetAndIncrementSequence`.
- **`WhenScenario`:** the condition or inputs that trigger it—e.g. `WhenPlayerIsUnknown`, `WhenDevPlayerPostsValidMove`.
- Omit redundant words when the scenario is already obvious; keep names readable in test runners and failure output.
```csharp
// Examples
public async Task GetPosition_ShouldReturnNotFound_WhenPlayerIsUnknown() { }
public async Task PostMove_ShouldReturnBadRequest_WhenSchemaVersionIsWrong() { }
```
## Unit and integration tests (Arrange, Act, Assert)
- Structure every test method as **Arrange → Act → Assert (AAA)**.
- Use a **`// Arrange`**, **`// Act`**, and **`// Assert`** comment (or the same words in a brief block comment) so the three phases are obvious at a glance.
- **Arrange:** create SUT dependencies, inputs, HTTP clients/factories, and test data. No assertions on the outcome under test (guards on arrange setup are optional and rare).
- **Act:** invoke the single behavior under test. For integration tests, the Act block may include a short sequence of calls if that sequence *is* the behavior (e.g. POST then GET to verify persistence)—keep it one clear scenario per test.
- **Assert:** all expectations (`Assert.*`, etc.); read response bodies here when the read is for verification, not for driving the next call (otherwise fold those reads into Act).
- Multi-scenario files stay readable with **one AAA triple per `[Fact]` / `[Theory]`** method.
- **Write-time default:** use the workspace VS Code snippet prefix **`xut`** (method) or **`xutc`** (class) from `.vscode/csharp.code-snippets` when authoring tests so AAA sections are present from the first draft.
- **Template reference:** `server/NeonSprawl.Server.Tests/TestTemplates/AAA_TEST_METHOD_TEMPLATE.cs.txt`.
## Tooling
- Prefer fixes that satisfy built-in **.NET analyzers** / `dotnet format` (if adopted) rather than fighting IDE warnings without reason.