C# Coding Standards
Preface
Rule of Thumb
- Readability first (your code should be your documentation most of the time)
- Follow IDE's auto formatted style unless you have really good reasons not to do so. (Ctrl + K + D in Visual Studio)
- Learn from existing code
References
This coding standards is inspired by these coding standards
IDE Helper
The settings that can imported into your IDE can be found here.
I. Main Coding Standards
-
Use Pascal casing for class and structs
class PlayerManager; struct PlayerData;
-
Use camel casing for local variable names and function parameters
public void SomeMethod(int someParameter) { int someNumber; int id; }
-
Use verb(base form)-object pairs for method names, by default.
public uint GetAge() { // function implementation... }
-
However, if a method simply returns a boolean state, the verb part of the name should be prefixed Is, Can, Has or Should. If the function name becomes not natural by doing so, use the 3rd-person singular form of another verb.
public bool IsAlive(Person person); public bool Has(Person person); public bool CanAccept(Person person); public bool ShouldDelete(Person person); public bool Exists(Person person);
-
Use pascal casing for all method names except (see below)
public uint GetAge() { // function implementation... }
-
Use camel case for any non-public method. You might need to add custom Visual Studio style rule as described here
private uint getAge() { // function implementation... }
-
Use ALL_CAPS_SEPARATED_BY_UNDERSCORE for constants
const int SOME_CONSTANT = 1;
-
Use static readonly if an object is a constant
public static readonly MyConstClass MY_CONST_OBJECT = new MyConstClass();
-
Use ALL_CAPS_SEPARATED_BY_UNDERSCORE for
static readonly
variables used as constants -
Use
readonly
when a variable must be assigned only oncepublic class Account { private readonly string mPassword; public Account(string password) { mPassword = password; } }
-
Use pascal casing for namespaces
namespace System.Graphics
-
prefix boolean variables with
b
.bool bFired; // for local variable private bool mbFired; // for private member variable
-
prefix boolean properties with
Is
,Can
,Should
orHas
.public bool IsFired { get; private set; } public bool HasChild { get; private set; } public bool CanModal { get; private set; } public bool ShouldRedirect { get; private set; }
-
prefix interfaces with
I
interface ISomeInterface;
-
prefix enums with
E
public enum EDirection { North, South }
-
prefix structs with
S
unless they arereadonly struct
spublic struct SUserID;
-
prefix
private
member variables withm
. Use Pascal casing for the rest of a member variablePublic class Employee { public int DepartmentID { get; set; } private int mAge; }
-
Methods with return values must have a name describing the value returned
public uint GetAge();
-
Use descriptive variable names. e.g
index
oremployee
instead ofi
ore
unless it is a trivial index variable used for loops. -
Capitalize every character in acronyms only if there is no extra word after them.
public int OrderID { get; private set; } public string HttpAddress { get; private set; }
-
Prefer properties over getter setter functions
BAD:
public class Employee { private string mName; public string GetName(); public string SetName(string name); }
GOOD:
public class Employee { public string Name { get; set; } }
-
Declare local variables as close as possible to the first line where it is being used.
-
Use precision specification for floating point values unless there is an explicit need for a
double
.float f = 0.5F;
-
Always have a
default
: case for aswitch
statement.switch (number) { case 0: ... break; default: break;
-
If
default
: case must not happen in aswitch
case, always addDebug.Fail();
switch (type) { case 1: ... break; default: Debug.Fail("unknown type"); break; }
-
Debug.Assert()
every assumptions you make while writing code. -
Names of recursive functions end with
Recursive
public void FibonacciRecursive();
-
Order of class variables and methods must be as follows:
- member variables
- properties (exception: if a private variable is accessed by a property, it should appear right before the mapped property)
- constructors
- methods (follow public to private order)
-
In a class, group relevant methods together. Aslo group relevant member variables together.
-
If parameter types are general, function overloading must be avoided
Use:
public Anim GetAnimByIndex(int index); public Anim GetAnimByName(string name);
Instead of:
public Anim GetAnim(int index); public Anim GetAnim(string name);
-
Each class must be in a separate source file unless it makes sense to group several smaller classes.
-
The filename must be the same as the name of the class including upper and lower cases.
public class PlayerAnimation {}
PlayerAnimation.cs
-
When a class spans across multiple files(i.e. partial classes), these files have a name that starts with the name of the class, followed by a dot and the subsection name.
public partial class Human;
Human.Head.cs Human.Body.cs Human.Arm.cs
-
Use
assert
for any assertion you have. Assert is not recoverable. (e.g, most function will haveDebug.Assert
(not null parameters) ) -
The name of a bitflag enum must be suffixed by
Flags
[Flags] public enum EVisibilityFlags { None = 0, Character = 1 << 0, Terrain = 1 << 1, Building = 1 << 2, }
-
Prefer overloading over default parameters
-
When default parameters are used, restrict them to natural immutable constants such as
null
,false
or0
. -
Shadowed variables are not allowed.
public class SomeClass { public int Count { get; set; } public void Func(int count) { for (int count = 0; count != 10; ++count) { // Use count } } }
-
Always use containers from
System.Collections.Generic
over ones fromSystem.Collections
. Using a pure array is fine as well. -
Use real type over implicit typing(i.e,
var
) unless the type is unimportant. Some acceptablevar
usage includesIEnumerable
and when thenew
keyword is used for anonymous type. -
Use
static
class, not singleton pattern -
Use
async Task
instead ofasync void
. The only place whereasync void
is allowed is for the event handler. -
Do not add
-Async
postfix forasync
methods. -
Validate any external data at the boundary and return before passing the data into our functions. This means that we assume all data is valid after this point.
-
Therefore, do not throw any exception from inside non-boundary methods. Also, exceptions should be handled at the boundary only.
-
As an exception to the previous rule, exception throwing is allowed when
switch
-default
is used to catch missingenum
handling logic. Still, do not catch this exceptionswitch (accountType) { case AccountType.Personal: return something; case AccountType.Business: return somethingElse; default: throw new NotImplementedException($"unhandled switch case: {accountType}"); }
-
Prefer not to allow
null
parameters in your function, especially from apublic
one. -
If
null
parameter is used, and postfix the parameter name withOrNull
public Anim GetAnim(string nameOrNull) { }
-
Prefer not to return
null
from any function, especially from apublic
one. However, you sometimes need to do this to avoid throwing exceptions. -
If
null
is returned from any function. Postfix the function name withOrNull
.public string GetNameOrNull();
-
Utilize in-line Lambda expressions exclusively for single, straightforward statements.
-
Avoid object initializer unless it is used with
required
modifier(C# 11.0) and init-only setter (C# 9.0) -
Declare the variable for an
out
parameter on a seprate line. Do NOT declare it int the argument list. -
Do not use the null coalescing operator, introduced in C# 7.0.
-
Do not use
using
declaration, introduced in C# 8.0. Useusing
statement instead. -
Always specify a data type after
new
keyword unless you are using annoymous type inside a function. -
Use private init-only setter(
private init
), introduced in C# 9.0, wherever possible. -
Use file scoped namespace declarations, introduced in C# 10.0.
-
When strong-typing a generic type, use
readonly record struct
, introduced in C# 10.0.
II. Code Formatting
-
Use Visual Studio's default for tabs. If another IDE is used, use 4 spaces instead of a real tab.
-
Always place an opening curly brace (
{
) in a new line -
Add curly braces even if there's only one line in the scope
if (bSomething) { return; }
-
Declare only one variable per line
BAD:
int counter = 0, index = 0;
GOOD:
int counter = 0; int index = 0;
III. Project Settings
-
For Release builds, treat compiler warnings as errors.
-
Do not use implicit global using (C# 10.0)
IV. Framework Specific Guidelines
A. Auto Serialization/Deserialization (e.g. System.Text.Json
)
-
Auto-serializable data must be defined as
class
. -
Auto-serializable
class
must not contain any library-specific attribute in it. -
All data in auto-serializable
class
must be declared/defined viapublic
auto properties. (1-to-1 mapping between properties and member variables) -
If you need a read-only property in auto-serializable
class
, make apublic
method instead. -
Auto-serializable
class
must have only onepublic
constructor. This constructor must not take any parameter. -
Do not directly call a auto-serialization method. (e.g.
JsonSerializer.Serialize<>()
). Make a wrapper method instead to limit the parameter types.
B. XAML Controls
-
Do not name (i.e,
x:name
) a control unless you absolutely need it -
Use pascal casing with prefixed
x
character for the name.xLabelName
-
Prefix the name with full control type
xLabelName xButtonAccept
C. ASP .NET Core
-
When using DTO(Data Transfer Object)s for a request body for a RESTful API, make each value-type property as
nullable
so that model validation is automatic[Required] public Guid? ID { get; set; }
-
Validate all the requests as the first thing in any controller method. Once validation passes, all inputs are assumed to be correct. So no
[required]
nullable
properties will benull
. -
Unlike above,
[RouteParam]
will not have?
```cs public bool GetUser([RouteParam]Guid userID)