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

Irurueta.Geometry, PointEstimator, Status.Pending

Method, property, event

PascalCase

ComputeDistance, IsEmpty, Completed

Public/protected field, constant

PascalCase

public const int MaxRetries = 3;

Private/internal field

_camelCase

private readonly int _count;

Private static field

s_camelCase (runtime style) or _camelCase

private static int s_instances;

Parameter, local

camelCase

void Scale(double factor)

Interface

I + PascalCase

IComparable, IReadOnlyList<T>

Type parameter

T or T + PascalCase

T, TKey, TResult

Attribute class

PascalCase + Attribute

ValidatedByAttribute

Async method

PascalCase + Async

LoadAsync

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. index beats i outside a tight loop; httpClient beats hc.

  • 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, not OrderList.

  • Do not prefix enums with the enum name (Status.Pending, not Status.StatusPending); name a [Flags] enum in the plural.

  • Avoid names that differ only by case — they are not usable from case-insensitive languages.

  • Use nameof rather 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 null for a collection — return an empty one.

  • Avoid out and ref parameters except in the established TryX pattern.

  • Provide a TryX alternative 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.

  • using directives at the top of the file, outside the namespace, System first.

  • 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

<summary>

One-sentence description. The single most important tag — it is what IntelliSense shows.

<remarks>

Longer explanation, caveats, thread-safety, performance notes.

<param name="x"> / <paramref name="x"/>

Describe a parameter / refer to one from prose.

<typeparam name="T"> / <typeparamref name="T"/>

The same, for type parameters.

<returns>

What the method returns, including the meaning of each possible value.

<value>

What a property represents.

<exception cref="T">

An exception the member throws, and the condition that causes it.

<see cref="M"/> / <seealso cref="M"/>

An inline / see-also cross-reference. cref is checked by the compiler.

<see langword="null"/>

A language keyword, rendered as code.

<example> / <code>

A worked example, and the code within it.

<para>, <list>, <c>

A paragraph, a list, and inline code.

<inheritdoc/>

Inherit the documentation of the base member or implemented interface.

<include file="…" path="…"/>

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:

  • readonly on every field that is not reassigned; sealed on every class not designed for inheritance.

  • String interpolation over concatenation and over string.Format.

  • switch expressions over if/else chains that produce a value.

  • record for immutable data carriers.

  • static on 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 .editorconfig on day one, and set EnforceCodeStyleInBuild so it is real.

  • Run dotnet format --verify-no-changes in CI; formatting arguments should never reach code review.

  • Turn on GenerateDocumentationFile for 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