5.7 KiB
5.7 KiB
| description | globs | alwaysApply |
|---|---|---|
| C# naming, layout, and primary constructors; Microsoft conventions and .NET idioms. | **/*.cs | true |
C# style (Neon Sprawl)
Follow Microsoft’s 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, usethis.or rename for clarity. - Static fields:
camelCasefor private/internal static fields;PascalCaseforpublic staticmembers (includingreadonly/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,
thisreferences 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. - 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.PositionStatefor files inGame/PositionState/.
Test method naming convention
- Use
MethodName_ShouldExpectedOutcome_WhenScenario(three segments,PascalCaseinside 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// Assertcomment (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.
Tooling
- Prefer fixes that satisfy built-in .NET analyzers /
dotnet format(if adopted) rather than fighting IDE warnings without reason.