134 lines
7.7 KiB
Markdown
134 lines
7.7 KiB
Markdown
---
|
||
description: C# naming, layout, and primary constructors; Microsoft conventions and .NET idioms.
|
||
globs: "**/*.cs"
|
||
alwaysApply: true
|
||
---
|
||
|
||
# C# style (Neon Sprawl)
|
||
|
||
Follow Microsoft’s **[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)
|
||
|
||
**Mandatory for every new or changed test method** in `*Tests.cs` (`[Fact]` / `[Theory]` bodies). Do not ship partial AAA (e.g. comments present but body read still in Act). Agents and humans must treat this as **merge-ready** layout, not a suggestion.
|
||
|
||
### Required layout
|
||
|
||
1. **Phase labels** — Each test method contains **`// Arrange`**, **`// Act`**, and **`// Assert`** exactly once, in that order, each on its own line (same indentation as the test body). Use these exact words so grep and review stay consistent. **No blank line is required** immediately after those comments; the first statement of each phase may follow on the next line.
|
||
|
||
2. **Arrange** — Factories, `HttpClient`, queued mock transports, DTOs, seeds, and other setup. Do **not** assert the **outcome under test** in Arrange (rare guards on arrange-only helpers are acceptable).
|
||
|
||
3. **Act** — Invoke the **behavior under test** only (e.g. one `PostAsJsonAsync`, `GetAsync`, or SUT call). If the scenario *is* a short sequence (e.g. POST then GET to prove persistence), keep the whole sequence in Act with **no** `Assert.*` between those calls.
|
||
|
||
4. **Assert** — All `Assert.*` (and any other outcome checks). **HTTP / deserialization:** when the next step is verifying the response (status already observed, body for assertions), perform **`ReadFromJsonAsync`** / similar **reads here**, not in Act beside the POST. Reads that **drive** the next call belong in Act, not Assert.
|
||
|
||
5. **One AAA triple per test method** — Each `[Fact]` or `[Theory]` case gets its own Arrange/Act/Assert; do not share a single Assert block across unrelated scenarios in one method.
|
||
|
||
### Authoring defaults
|
||
|
||
- Use VS Code snippet **`xut`** (method) or **`xutc`** (class) from `.vscode/csharp.code-snippets`, or copy **`server/NeonSprawl.Server.Tests/TestTemplates/AAA_TEST_METHOD_TEMPLATE.cs.txt`**.
|
||
|
||
### Example (minimal HTTP integration)
|
||
|
||
```csharp
|
||
[Fact]
|
||
public async Task PostExample_ShouldReturnOk_WhenBodyValid()
|
||
{
|
||
// Arrange
|
||
await using var factory = new InMemoryWebApplicationFactory();
|
||
var client = factory.CreateClient();
|
||
var request = new { schemaVersion = 1 };
|
||
|
||
// Act
|
||
var response = await client.PostAsJsonAsync("/game/example", request);
|
||
|
||
// Assert
|
||
var body = await response.Content.ReadFromJsonAsync<ExampleResponse>();
|
||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||
Assert.NotNull(body);
|
||
}
|
||
```
|
||
|
||
## Tooling
|
||
|
||
- Prefer fixes that satisfy built-in **.NET analyzers** / `dotnet format` (if adopted) rather than fighting IDE warnings without reason.
|