Coding Conventions and Documentation
|
This section documents C# 14 on .NET 10 (LTS), as published at learn.microsoft.com/dotnet/csharp, which is the reference these pages are written and verified against. Features introduced by C# 15 / .NET 11 are still in preview and are always flagged as such — never presented as baseline. This content was generated with the assistance of AI and should be verified against learn.microsoft.com before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
C# has an unusually strong house style. Microsoft publishes both coding conventions (how code is written — naming, layout, which construct to prefer) and framework design guidelines (how public APIs are designed), and the overwhelming majority of C# codebases follow them. Following them too is worth far more than any local preference: it makes your code readable by everyone who has read any other C#, and it makes every analyzer, refactoring and IDE suggestion agree with you rather than fight you.
Naming
Identifier Casing
| Element | Convention | Example |
|---|---|---|
Namespace, type, enum member |
PascalCase |
|
Method, property, event |
PascalCase |
|
Public/protected field, constant |
PascalCase |
|
Private/internal field |
|
|
Private static field |
|
|
Parameter, local |
camelCase |
|
Interface |
|
|
Type parameter |
|
|
Attribute class |
PascalCase + |
|
Async method |
PascalCase + |
|
Note the two divergences from Java that trip people up: methods and properties are PascalCase, and interfaces
carry the I prefix. See the Java Reference for the comparison.
Naming Rules
-
Use meaningful, pronounceable names; prefer clarity to brevity.
indexbeatsioutside a tight loop;httpClientbeatshc. -
Do not use Hungarian notation, and do not encode the type in the name (
strName,iCount). -
Do not abbreviate, except for universally-known acronyms. Two-letter acronyms stay uppercase (
IOStream,ID); longer ones are PascalCase (HtmlParser,XmlReader,HttpClient). -
Name a boolean member as a predicate:
IsEnabled,HasItems,CanExecute,ShouldRetry. -
Name a collection in the plural:
Orders, notOrderList. -
Do not prefix enums with the enum name (
Status.Pending, notStatus.StatusPending); name a[Flags]enum in the plural. -
Avoid names that differ only by case — they are not usable from case-insensitive languages.
-
Use
nameofrather than a string literal whenever a member name is needed:
public sealed class Order
{
private readonly int _quantity;
public Order(int quantity)
{
// nameof survives a rename; a string literal does not.
ArgumentOutOfRangeException.ThrowIfNegative(quantity, nameof(quantity));
_quantity = quantity;
}
public int Quantity => _quantity;
}
Framework Design Guidelines
The naming rules above are about spelling. The design guidelines are about shape, and they matter for any type another team will consume.
Type design. Prefer classes to interfaces for extensibility points that may need to grow; an interface
cannot gain a member without breaking implementers (default interface members soften but do not remove this).
Seal a class unless you have designed for inheritance — and if you have, document what a derived type must
preserve. Use a struct only for small, immutable, value-like data (see
Structs and Value Types).
Member design.
-
Prefer properties to fields in public APIs; prefer a property to a
GetX()/SetX()pair. -
Use a method, not a property, when the operation is expensive, has side effects, may throw, or returns a different object each call.
-
Validate arguments at every public entry point and throw the standard exception type (
ArgumentNullException,ArgumentOutOfRangeException,ArgumentException) naming the parameter. -
Keep parameter lists short; past three or four, introduce an options object.
-
Prefer the most general practical parameter type (
IEnumerable<T>) and the most specific practical return type (IReadOnlyList<T>). -
Do not return
nullfor a collection — return an empty one. -
Avoid
outandrefparameters except in the establishedTryXpattern. -
Provide a
TryXalternative wherever failure is expected rather than exceptional.
public sealed class Catalogue
{
private readonly Dictionary<string, string> _entries = new();
// General in, specific out; never null.
public IReadOnlyList<string> Find(IEnumerable<string> keys) =>
keys.Where(_entries.ContainsKey).ToList();
// Exceptional failure: throw.
public string Get(string key) =>
_entries.TryGetValue(key, out string? value)
? value
: throw new KeyNotFoundException($"No entry for '{key}'.");
// Expected failure: the Try pattern, no exception.
public bool TryGet(string key, out string? value) => _entries.TryGetValue(key, out value);
}
Exception design. Throw the most specific existing exception type; define a new one only when callers need
to catch it distinctly. Never throw Exception, SystemException or ApplicationException. See
Exceptions and Error Handling.
Layout
The conventions the IDE’s default formatting applies, and that the rest of the ecosystem expects:
-
Four spaces per indent level; no tabs.
-
Allman braces — the opening brace on its own line — for types, methods and blocks.
-
Braces even around single-statement bodies.
-
One statement and one declaration per line.
-
A blank line between members; no blank line after an opening brace.
-
usingdirectives at the top of the file, outside the namespace,Systemfirst. -
A line length limit (commonly 120 characters) applied consistently.
using System;
using System.Collections.Generic;
namespace Irurueta.Sample.Layout;
public sealed class Formatter
{
private readonly IReadOnlyList<string> _parts;
public Formatter(IReadOnlyList<string> parts)
{
ArgumentNullException.ThrowIfNull(parts);
_parts = parts;
}
public string Render(string separator)
{
if (_parts.Count == 0)
{
return string.Empty;
}
return string.Join(separator, _parts);
}
}
.editorconfig
.editorconfig at the repository root makes the conventions enforceable rather than aspirational: the IDE
formats to them, dotnet format fixes them, and the build can fail on them. The C# entries are an extension
of the cross-editor standard:
root = true
[*]
indent_style = space
insert_final_newline = true
charset = utf-8
[*.{cs,csx}]
indent_size = 4
max_line_length = 120
# Formatting.
csharp_new_line_before_open_brace = all
csharp_indent_case_contents = true
csharp_space_after_keywords_in_control_flow_statements = true
dotnet_sort_system_directives_first = true
csharp_using_directive_placement = outside_namespace:warning
# Language style preferences, each with a severity.
csharp_style_namespace_declarations = file_scoped:warning
csharp_style_var_for_built_in_types = false:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
csharp_style_expression_bodied_methods = when_on_single_line:suggestion
csharp_prefer_braces = true:warning
dotnet_style_null_propagation = true:suggestion
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
csharp_style_prefer_pattern_matching = true:suggestion
dotnet_style_readonly_field = true:warning
# Naming: private fields must be _camelCase.
dotnet_naming_rule.private_fields_underscore.symbols = private_fields
dotnet_naming_rule.private_fields_underscore.style = underscore_camel
dotnet_naming_rule.private_fields_underscore.severity = warning
dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private
dotnet_naming_style.underscore_camel.required_prefix = _
dotnet_naming_style.underscore_camel.capitalization = camel_case
# Individual analyzer rules.
dotnet_diagnostic.CA1062.severity = warning # validate public arguments
dotnet_diagnostic.IDE0055.severity = warning # formatting
Each style rule carries a severity — none, silent, suggestion, warning or error — which is what
decides whether it merely shows in the IDE or fails the build. Setting
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> makes the IDExxxx style rules run during dotnet
build, not just in the editor.
Analyzers and dotnet format
Roslyn analyzers check code as it compiles. The .NET SDK ships the CA rules (design, reliability,
performance, security, globalization) and the IDE rules (style); third-party packages such as
StyleCop.Analyzers add more. They are configured per rule in .editorconfig, and turned on in the project:
<PropertyGroup>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>latest</AnalysisLevel>
<AnalysisMode>Recommended</AnalysisMode>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>
dotnet format applies the fixable subset automatically:
dotnet format # whitespace, style and analyzer fixes
dotnet format --verify-no-changes # CI gate: fail if anything would change
dotnet format style --severity warn
dotnet format analyzers --diagnostics CA1062
Suppress a rule where it is genuinely wrong, narrowly and with a reason — [SuppressMessage] on the member,
or #pragma warning disable around the statement, never a blanket <NoWarn>:
using System.Diagnostics.CodeAnalysis;
public static class Suppressions
{
[SuppressMessage(
"Globalization", "CA1305:Specify IFormatProvider",
Justification = "This value is a protocol token, never displayed to a user.")]
public static string Format(int value) => value.ToString();
}
XML Documentation Comments
A /// comment above a member becomes structured XML the compiler extracts into a .xml file alongside the
assembly. That file is what drives IntelliSense quick info, DocFX output, and NuGet package documentation:
namespace Irurueta.Sample.Documented;
/// <summary>
/// Computes summary statistics over a sequence of samples.
/// </summary>
/// <remarks>
/// Instances are immutable and safe to share between threads. For streaming data, prefer
/// <see cref="StreamingStatistics"/>, which does not retain the samples.
/// </remarks>
public sealed class Statistics
{
private readonly IReadOnlyList<double> _samples;
/// <summary>
/// Initializes a new instance of the <see cref="Statistics"/> class.
/// </summary>
/// <param name="samples">The samples to summarise. The sequence is copied.</param>
/// <exception cref="ArgumentNullException"><paramref name="samples"/> is <see langword="null"/>.</exception>
public Statistics(IEnumerable<double> samples)
{
ArgumentNullException.ThrowIfNull(samples);
_samples = samples.ToList();
}
/// <summary>
/// Gets the number of samples.
/// </summary>
/// <value>The sample count, which is never negative.</value>
public int Count => _samples.Count;
/// <summary>
/// Computes the arithmetic mean of the samples.
/// </summary>
/// <returns>The mean of the samples.</returns>
/// <exception cref="InvalidOperationException">The instance holds no samples.</exception>
/// <example>
/// The following example computes the mean of three samples:
/// <code>
/// var statistics = new Statistics([1.0, 2.0, 3.0]);
/// Console.WriteLine(statistics.Mean()); // 2
/// </code>
/// </example>
/// <seealso cref="Count"/>
public double Mean() =>
_samples.Count > 0
? _samples.Average()
: throw new InvalidOperationException("No samples.");
}
/// <summary>
/// Computes statistics incrementally, without retaining the samples.
/// </summary>
public sealed class StreamingStatistics
{
}
The Tags
| Tag | Use |
|---|---|
|
One-sentence description. The single most important tag — it is what IntelliSense shows. |
|
Longer explanation, caveats, thread-safety, performance notes. |
|
Describe a parameter / refer to one from prose. |
|
The same, for type parameters. |
|
What the method returns, including the meaning of each possible value. |
|
What a property represents. |
|
An exception the member throws, and the condition that causes it. |
|
An inline / see-also cross-reference. |
|
A language keyword, rendered as code. |
|
A worked example, and the code within it. |
|
A paragraph, a list, and inline code. |
|
Inherit the documentation of the base member or implemented interface. |
|
Pull documentation from an external XML file. |
<inheritdoc/> deserves emphasis: an override or interface implementation should almost always use it rather
than duplicating prose that will drift:
namespace Irurueta.Sample.Inheriting;
/// <summary>
/// Converts values to their display form.
/// </summary>
public interface IRenderer
{
/// <summary>
/// Renders <paramref name="value"/> for display.
/// </summary>
/// <param name="value">The value to render.</param>
/// <returns>The rendered text.</returns>
string Render(object value);
}
/// <summary>
/// Renders values using the invariant culture.
/// </summary>
public sealed class InvariantRenderer : IRenderer
{
/// <inheritdoc/>
public string Render(object value) =>
Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture) ?? string.Empty;
}
Turning It On
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- CS1591: missing XML comment on a public member. Keep it on for a library. -->
<NoWarn>$(NoWarn)</NoWarn>
</PropertyGroup>
GenerateDocumentationFile also enables warning CS1591 for every undocumented public member, which is the
mechanism that keeps a library’s documentation complete. For an application, suppressing CS1591 is reasonable;
for a published library it is not.
Generating Documentation with DocFX
DocFX turns the generated XML plus hand-written Markdown into a static documentation site. It is the tool Microsoft’s own .NET API browser is built on:
dotnet tool install -g docfx
docfx init --yes # scaffolds docfx.json, a docs/ folder and a toc.yml
docfx docfx.json --serve # generates the site and serves it locally
{
"metadata": [
{
"src": [ { "files": [ "src/**/*.csproj" ] } ],
"dest": "api"
}
],
"build": {
"content": [
{ "files": [ "api/**.yml", "api/index.md" ] },
{ "files": [ "articles/**.md", "toc.yml", "*.md" ] }
],
"dest": "_site"
}
}
The metadata stage reflects over the projects and their XML files to produce API YAML; the build stage
renders that plus the hand-written articles into HTML. Because the API reference comes from the XML comments,
the documentation is only as good as the /// comments in the source — which is the argument for treating
CS1591 as an error in a library.
Idioms
The constructs experienced C# developers reach for, and why.
Guard clauses at the top of a method, using the modern throw helpers, so the body reads without nesting:
public static class Guards
{
public static string Normalise(string? text, int maxLength)
{
ArgumentException.ThrowIfNullOrWhiteSpace(text);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxLength);
return text.Trim()[..Math.Min(text.Trim().Length, maxLength)];
}
}
Expression-bodied members for anything that is genuinely one expression — and a block body for anything that is not:
public sealed class Temperature(double celsius)
{
public double Celsius { get; } = celsius;
public double Fahrenheit => Celsius * 9 / 5 + 32;
public override string ToString() => $"{Celsius:F1} °C";
}
var where the type is apparent, an explicit type where it is not. The convention most teams settle on:
var for new expressions and casts, an explicit type for method results whose type is not obvious from the
line:
public static class VarGuidance
{
public static void Show(IEnumerable<string> source)
{
var builder = new System.Text.StringBuilder(); // obvious
var items = new List<string>(source); // obvious
int count = items.Count; // explicit: small and clear
IReadOnlyList<string> result = items.AsReadOnly(); // explicit: the type is the point
Console.WriteLine((builder.Length, count, result.Count));
}
}
is null and is not null rather than ==/!=, because the pattern cannot be intercepted by an
overloaded == and reads unambiguously:
public static class NullChecks
{
public static string Describe(object? value) =>
value is null ? "nothing"
: value is string text ? $"text: {text}"
: "something";
}
Target-typed new where the type is already stated on the left, and collection expressions for
collections:
public sealed class Configured
{
private readonly Dictionary<string, List<int>> _groups = new();
private readonly string[] _defaults = ["alpha", "beta"];
public void Add(string key, int value)
{
if (!_groups.TryGetValue(key, out List<int>? values))
{
values = [];
_groups[key] = values;
}
values.Add(value);
}
public IReadOnlyList<string> Defaults => _defaults;
}
A few more worth adopting wholesale:
-
readonlyon every field that is not reassigned;sealedon every class not designed for inheritance. -
String interpolation over concatenation and over
string.Format. -
switchexpressions overif/elsechains that produce a value. -
recordfor immutable data carriers. -
staticon lambdas that capture nothing. -
One public type per file, named after the file.
Practical Guidance
-
Adopt the Microsoft conventions wholesale rather than inventing a local style; the ecosystem’s tooling assumes them.
-
Commit a
.editorconfigon day one, and setEnforceCodeStyleInBuildso it is real. -
Run
dotnet format --verify-no-changesin CI; formatting arguments should never reach code review. -
Turn on
GenerateDocumentationFilefor libraries and treat CS1591 as an error. -
Document why, not what: the summary says what a member is for, and
<remarks>says what a caller needs to know that the signature does not say. -
Use
<inheritdoc/>for overrides and implementations. -
Suppress a rule only at the narrowest scope, always with a
Justification.
See Also
-
Preprocessor Directives and Compilation — the compiler options that enforce these settings.
-
Build and Tooling —
dotnet format, analyzers and the wider toolchain. -
Exceptions and Error Handling — the guidelines on throwing.
-
Classes and Objects — the member design these conventions apply to.
-
Java Reference — for readers comparing naming conventions.