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

6.5 KiB
Raw Blame History

description globs alwaysApply
C# naming, layout, and primary constructors; Microsoft conventions and .NET idioms. **/*.cs true

C# style (Neon Sprawl)

Follow Microsofts C# Coding Conventions and C# identifier rules. 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.
// 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.
// 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.csNeonSprawl.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.
// 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.