C# coding style guide
This is the normative coding style guide for the C#/.NET code in EncDotNet.S100. It exists so that style is explicit and documented rather than inferred from surrounding code. New code must follow it, and existing code is migrated toward it opportunistically.
Scope. This document covers language, formatting, and naming style. For API-shape decisions (collection return types,
classvsrecord, quantity types) see the companion API design conventions. For test conventions seeCONTRIBUTING.md.
Enforcement. The mechanically-checkable rules below are encoded in the repository
.editorconfigand can be checked/applied withdotnet format. CI's Format check job uses one targeted invocation to verify whitespace/formatting, using directives (IDE0005: System-first ordering, placement outside the file-scoped namespace, and removal of unnecessary directives), and naming (IDE1006). IDE1006 is verification only becausedotnet formatcannot Fix-All naming; naming failures must be corrected manually. Other style diagnostics remain ungated because the solution-wide style fix-all is unreliable on the multi-targeted (net8.0;net10.0) projects.EnforceCodeStyleInBuildis left off sodotnet buildstays fast and does not turn style suggestions into build errors. When this document and the.editorconfigdisagree, the encoded rule wins and this document should be corrected.
1. Guiding principles
- Consistency over personal preference. Match the established style even if you would personally choose differently. A uniform codebase is easier to read, review, and refactor mechanically.
- The compiler is the first reviewer. The build sets
TreatWarningsAsErrors=true(seeDirectory.Build.props); a warning is a failure. Do not suppress warnings to make the build pass — fix the cause. Avalonia's XAML compiler ignores that setting, so its warning-levelAVLN*diagnostics are promoted to errors in.editorconfiginstead. - Nullability is part of the type. Nullable reference types are enabled solution-wide. Model presence/absence honestly instead of defeating the analyzer.
- Prefer the standard tooling. Formatting is whatever
dotnet formatproduces from the shared.editorconfig; do not hand-format against it.
2. Files, namespaces, and usings
One top-level type per file, and the file name matches the type (
SpecRef.cscontainsSpecRef). Small tightly-coupled helpers (a private nested type, a partial file) are the exception.File-scoped namespaces only:
namespace EncDotNet.S100.Core;Block-scoped (
namespace X { ... }) namespaces are not used anywhere in the codebase and must not be introduced.ImplicitUsingsis enabled, so do not addusingdirectives for the implicit set (System,System.Linq,System.Collections.Generic, etc.). Add only the usings the implicit set does not cover.Place
usingdirectives at the top of the file,System.*first, then other namespaces, each group alphabetically ordered. Remove unused usings (they are warnings, and warnings fail the build).Use UTF-8, LF or platform-native line endings per
.editorconfig, a trailing newline, and no trailing whitespace.
3. Formatting
4 spaces per indent level. No tabs.
Allman braces — the opening brace goes on its own line for types, methods, properties, and control-flow blocks:
public SpecRef(string name, SpecVersion edition) { Name = SpecName.Normalize(name); Edition = edition; }Always brace multi-line blocks. A single-statement
ifwhose body is on the same line is acceptable for terse guard clauses (if (firstDot <= 0) return false;), but once the body wraps to the next line it must be braced. Do not mix a braced and unbraced arm in the sameif/else.One statement per line; one declaration per line.
Keep lines reasonably short (roughly 100–120 columns). Prose in doc comments in this codebase wraps around 72–76 columns — match the surrounding file.
Use a single blank line to separate members and logical groups; never stack multiple consecutive blank lines.
Use expression-bodied members for one-liners where they read well (
public override string ToString() => $"{Name}/{Edition}";). Use a block body when the logic spans multiple statements.
4. Naming
| Element | Convention | Example |
|---|---|---|
| Namespace, type, method, property, event, enum member | PascalCase |
CoveragePipeline, TryParse |
| Interface | PascalCase prefixed with I |
IAssetSource |
| Type parameter | PascalCase prefixed with T |
TModel, TKey |
| Local variable, local constant, parameter | camelCase |
versionPart, tolerance, edition |
| Private / internal field (instance or mutable static) | _camelCase |
_assetSource, _tileWorkerCount |
Constant, static readonly |
PascalCase |
MaxDepthBands, Sampling |
| Async method | PascalCase suffixed Async |
OpenFeatureCatalogueAsync |
- The
s_/t_prefixes (the dotnet/runtime convention for static and thread-static fields) are not used here: a private field is_camelCaseregardless of whether it is instance or static, and astatic readonly/constfield isPascalCase. When astatic readonlyfield backs a same-named property, method, or type, give the field a distinct descriptivePascalCasename (for example,LazyDefaultbackingDefault). - Do not prefix with
this.to disambiguate fields — the_field prefix already makes fields visually distinct, andthis.is effectively unused in the codebase. - Prefer descriptive names over abbreviations, except well-known domain terms
(
Fc,Pc,Crs,Utm, S-100 attribute acronyms). Spec-derived identifiers should match the spec's casing where practical and cite the section (see §8).
5. Language features and idioms
varis the default for local declarations — it is used pervasively. Reach for an explicit type only when it materially improves readability or the right-hand side does not make the type obvious.- Use target-typed
newwhere the type is already stated (SpecRef value = new(name, edition);/= []for an empty collection). - Prefer collection expressions (
[],[a, b, c]) and range/index operators (s[..sep],afterPrefix[(firstDot + 1)..]) — both are used throughout. - Prefer pattern matching and switch expressions over long
if/else ifladders andswitchstatements where they read more clearly. - Prefer string interpolation (
$"...") over concatenation. Pass an explicitStringComparisonto string comparisons/StartsWith/IndexOfwhere culture matters (StringComparison.OrdinalIgnoreCasefor identifiers/tokens). - Use
is null/is not nullfor reference equality checks.
6. Nullability and argument validation
Never use the null-forgiving operator
!to silence the analyzer. If a value is genuinely never null, express that in the type; if it can be null, handle it.!is reserved for the rare, commented case where an invariant cannot be encoded.Validate public-entry-point arguments up front:
ArgumentNullException.ThrowIfNull(source); ArgumentException.ThrowIfNullOrWhiteSpace(name);Follow the
Parse/TryParsepair pattern for parsing:TryParsereturnsbooland never throws on malformed input;Parsedelegates to it and throwsFormatException(seeSpecRef).Throw the most specific standard exception (
ArgumentException,ArgumentOutOfRangeException,InvalidOperationException,FormatException,NotSupportedException) with a message that names the offending value.
7. Documentation comments
- Every public and protected member carries XML doc comments — at minimum
<summary>, plus<param>,<returns>, and<exception>where applicable. This is required, not optional. - Use
<see cref="..."/>for cross-references,<c>...</c>for inline code, and<remarks>/<para>for rationale. Explain why, not just what, for non-obvious design choices (seeSpecReffor the house style). - Internal/private members are documented when the intent is non-obvious.
- Implementation comments are for clarification only. Do not narrate code that speaks for itself. Prefer a comment that captures a non-obvious invariant, a spec reference, or a "do not change this without …" warning.
8. Spec-derived code
- For constants, enums, attribute names, group paths, and element names taken
from an IHO S-100 product specification, cite the spec and section in the XML
doc comment (e.g.
S-104 §10.2.3 WaterLevel attribute names). - Consult the matching per-spec skill/instruction file before writing
spec-semantic code (see the routing table in
CONTRIBUTING.md).
9. Async
- Suffix async methods with
Asyncand returnTask/Task<T>/ValueTask<T>. - Accept and honor a
CancellationTokenon async APIs that do I/O; flow it through to the calls you make. - Do not expose
async voidexcept for event handlers. - Avoid
.Result/.Wait()/.GetAwaiter().GetResult()— they invite deadlocks. Make the call chain async instead.
10. Dependencies and build
- NuGet versions are managed centrally in
Directory.Packages.propsvia Central Package Management. Do not addVersionattributes to individual.csprojfiles. - Do not add
#pragma warning disableor<NoWarn>to work around analyzer findings without a comment justifying the specific, local reason. - Keep code cross-platform (CI builds on
ubuntu-latest). Gate any platform-specific API to the appropriate runtime identifier.
This guide was delivered in phases: Phase 1 ("Define") wrote it, Phase 2
("Encode") captured the mechanically-checkable rules in .editorconfig,
Phase 3 ("Enforce") added the CI format gates, and Phase 4 ("Refactor")
brought existing code into whitespace, using-directive, and naming compliance.
CI now gates whitespace, IDE0005, and IDE1006; other style rules remain
hand-followed due to the multi-target constraint noted above.