This document describes the preferred C# coding style in our projects. It is based on the .NET Runtime Coding Style with some targeted modifications to improve readability and maintainability of the codebases, as well as to reduce potential for bugs being overlooked in code review.
The goal of this document is to provide a modern and consistent coding style across the codebase, reducing boilerplate where possible, while considering the readability and maintainability of the code, especially for code reviews where no intellisense or other tool-assisted context is available. The fundamental principle is that the code should be easy to read and understand at a glance, without having to search for the type of a variable or method return type. Similarly, it should be easy to distinguish between constants, fields, properties, statics, and other types of members.
The general rules we follow are "use Visual Studio defaults" and "readability over convenience", since code is read more often than it is written. Still, we aim to reduce boilerplate where possible and eliminate redundancy in the codebase.
- We use Allman style braces, where each brace begins on a new line. The only exceptions to this rule are auto-implemented properties (i.e.
public int Foo { get; set; }), simple object initializers (i.e.Person p = new() { Name = "John" };), and empty-bodied block statements (i.e.() => {}(No-op lambda), orfor (node = first; node != null; node = node.Next) {}). - We use four spaces of indentation (no tabs) to maintain visual consistency across different editors and platforms where tab width may vary.
- We communicate visibility and scope through naming conventions:
- types, properties, and all methods (including local ones) are in
PascalCaseto distinguish them from fields and locals, ruling them out as potential targets for ByRefs (therefkeyword). - local variables and parameters are in
camelCase. - non-public instance fields are in
_camelCaseto distinguish them from locals and properties.- except: the member is a primary constructor parameter, in which case it is in
camelCase. For larger classes, consider using explicit private fields in_camelCaseto avoid confusion with local variables.
- except: the member is a primary constructor parameter, in which case it is in
- public fields must only be used in
structsand only where they are advantageous over properties for performance or interop reasons. Public fields should be named inPascalCasewith no prefix when used. - static fields have an
s_prefix to distinguish them from instance fields:s_camelCase. - thread static fields have a
t_prefix to clearly indicate their scope:t_camelCase. - thread-local instance fields have a
_th_prefix to clearly indicate their scope:_th_camelCase. - async-local instance fields have a
_al_prefix to clearly indicate their scope:_al_camelCase. This can be combined with the static prefix for static async-local fields:s_al_camelCase. - all compile-time constants are in
SCREAMING_SNAKE_CASEto communicate their immutability and distinguish them from properties.
- types, properties, and all methods (including local ones) are in
- We restrict visibility and modification as much as possible: hiding implementation details from the outside by making them
privateorinternal, and declaring fields asreadonlywhere possible. This communicates the intent of the code and reduces the risk of accidental modification. - We avoid
this.unless when accessing primary constructor parameters for a clear distinction between fields and locals. - We always specify the visibility, even if it's the default (e.g.
private string _foonotstring _foo). Visibility should be the first modifier (e.g.public abstractnotabstract public). - Namespace imports should be specified at the top of the file, outside of
namespacedeclarations, and should be sorted alphabetically. We use file-scoped namespaces (namespace Foo;instead ofnamespace Foo {...}) to avoid excessive indentation. - Avoid more than one empty line, as well as any number of trailing white spaces. Enable 'View White Space' in Visual Studio to detect these issues.
- File names should be named after the type they contain, for example
class Fooshould be inFoo.cs. Every file should contain at most one top-level type, although it may contain nested types and additional file-local type (e.g.class Foo { class Bar { } }andclass Foo {} file class Bar {}).- except: when working with very tightly coupled types, where it makes sense to have them in the same file for easier navigation and understanding. In such cases, it is acceptable to have multiple top-level types in the same file.
- except: when working generic types that would conflict with non-generic types of the same name, append
.Tto the file name, e.g.class FooinFoo.csandclass Foo<T>inFoo.T.cs. If multiple generic types exist, use.T1,.T2, etc, where the number corresponds to the number of generic type parameters, e.g.class Foo<T>inFoo.T1.csandclass Foo<TKey, TValue>inFoo.T2.cs.
- Avoid
var, even when the type is obvious (to maintain consistency). Explicit types improve readability, reduce the risk of bugs, and allow reviewers and other developers to understand code at a glance, which is especially important in code reviews where no intellisense is available.- Why? In conjunction with rule 11, disallowing usage of
varprevents bugs where the type is not what the developer expected, which is especially common in async contexts. For example:
[HttpGet] public IActionResult GetFoo() { var foo = dbContext.Set<Foo>().FirstOrDefaultAsync(); if (foo == null) { return NotFound(); } // we expect foo to be a Foo but it's actually a Task that was never awaited :C return Ok(foo); }
- The argument that
varreduces boilerplate is less relevant in modern C# versions where target-typednew()is available, which we use where possible but only when the type is explicitly named on the left-hand side, e.g.,FileStream stream = new(...);, but notstream = new(...);(where the variable was declared on a previous line). - in foreach loops, we can use tuple deconstruction to avoid the need for
varand make the code more readable, e.g.,foreach ((int key, string value) in dictionary) { ... }. - In all other cases, we still prefer explicit types over
varfor readability and consistency, even if it brings the inconvenience of having to type more characters while writing code. - The only exception to this rule is when the type is truly anonymous, e.g.,
var x = dbContext.Set<Foo>().Select(foo => new { Name, Age }).First(). Still, consider using afile-scoped (struct) type in such cases to make the code more readable and maintainable.
- Why? In conjunction with rule 11, disallowing usage of
- When writing task-returning methods, we postfix method names with
Async(e.g.public async Task<int> FooAsync()instead ofpublic async Task<int> Foo()). This clearly communicates the asynchronous nature of the method to the caller and helps reduce cases of asynchronous methods not being awaited. - We use language keywords instead of BCL types (e.g.
int, string, floatinstead ofInt32, String, Single, etc) for both type references as well as method calls (e.g.int.Parseinstead ofInt32.Parse). - We use
nameof(...)instead of"..."whenever possible and relevant. Similarly, we usestring.Emptyinstead of"". - Fields should be specified at the top within type declarations. The order of field groups should be
const,static,readonly, theninstancefields. This helps keep the fields organized and keeps fields that change more frequently closer to the implementation details. - When including non-ASCII characters in the source code use Unicode escape sequences (
\uXXXX) instead of literal characters. Literal non-ASCII characters occasionally get garbled by a tool or editor. - When using labels (for goto), indent the label one less than the current indentation, and name labels with
SCREAMING_SNAKE_CASE.- We strictly discourage the use of
gotofor control flow, except in the following cases, iff it improves overall readability:- Breaking out of deeply nested loops, where
breakandcontinuedon't suffice and the alternative would be to introduce one or more boolean flags that would make the code less readable. - In
bool TryGet(out Foo? foo)methods or similar cases where a distinct failure path with additional cleanup is required (e.g. assigning default values to out parameters) it is acceptable to define aFAILURElabel after the primaryreturn trueof the method and usegoto FAILUREafter each check that would result in a failure. - In very rare cases where
gotois the most readable and maintainable solution. You should be prepared to explain your reasoning in code review and consider whether there is a better way to structure the code.
- Breaking out of deeply nested loops, where
- We strictly discourage the use of
- Make all internal and private types static or sealed unless derivation from them is required. As with any implementation detail, they can be changed if/when derivation is required in the future.
- We avoid magic strings and numbers in our code, especially when they are used more than once. Instead, we use constants or readonly fields to define these values. When passing fixed values to a method, we prefer to use named arguments to make the code more readable and self-explanatory, e.g.,
DeflateStream deflate = new(fileStream, CompressionMode.Compress, leaveOpen: true)instead ofDeflateStream deflate = new(fileStream, CompressionMode.Compress, true). - The recommended order of modifiers is
public,private,internal,protected,file,static,extern,const,required,async,sealed,override,virtual,new,abstract,readonly,partial,unsafe, andvolatile. This order keeps the code consistent and lists modifiers in order of importance when reading the code from left to right. For example, visibility is more important than knowing whether a method isvirtualor its implementation details requireunsafecode. - We use
#regionand#endregiondirectives sparingly and only in very large files where splitting the file into separate files is not feasible. If used, regions should always be named (#region Name, and#endregion Name). - We use nullable reference types in all new projects globally and for new code in existing projects. We use nullable annotations to indicate when a value can be null and when it cannot. This especially includes
outparameters ofbool TryGet...(out Foo? value)where[NotNullWhen(...)]or[MaybeNullWhen(...)]attributes should be specified for theoutparameter. - All types, members, and variables should be named in a way that clearly communicates their purpose and intent. This includes avoiding abbreviations, using full words instead of acronyms, and using descriptive names that make the code self-explanatory. This is especially important for public APIs and interfaces, where the names should be clear and concise to make the code easy to understand and use. Exceptions to this rule are common acronyms and abbreviations that are widely understood in the context of the codebase and single-letter variable names for loop counters and similar short-lived variables. The larger the scope of the variable, the more descriptive the name should be.
- When using acronyms, we use PascalCase by default, e.g.,
XmlDocument,HttpRequest, etc. Only when the acronym is two letters long, capitalization is also acceptable, e.g.,IPAddress. - Generic type parameters should always start with
Tfollowed by a descriptive name, e.g.,TKeyandTValuefor dictionary types. Only in cases without ambiguity, the single letterTcan be used, e.g.,List<T>. - Interface names should be prefixed with
I, e.g.,IEnumerable,IDisposable,IList, etc.
- When using acronyms, we use PascalCase by default, e.g.,
- We DO NOT use Hungarian notation or other type prefixes in our code. The context of the code should make the type clear, and the use of descriptive names should eliminate the need for type prefixes. The only (and rare) exception to this rule is when working with handles or pointers, where handles should be prefixed with
hand pointers withp, e.g.,IntPtr hInstance,void* pData. This convention coming from the C/C++ world makes it absolutely clear in all contexts that the variable is an unmanaged resource that may require special handling. - Exceptions to above naming rules may be made under special circumstances where other conventions apply, such as in low-level P/Invoke interop scenarios where unmanaged structures and P/Invoke stubs should follow native naming conventions. Note that these internal types should never be exposed to public APIs and even internal calls should be wrapped where possible.
An EditorConfig file (.editorconfig) has been provided alongside this document, enabling C# auto-formatting conforming to the above guidelines.