Naming Conventions
PascalCase for types and public members, camelCase for locals and parameters, _camelCase for private fields.
class InvoiceService { private readonly int _accountId; public int GetTotal() { ... } }The compiler checks almost nothing about names. It only requires that an identifier starts with a letter or an underscore and continues with letters, digits or a few connecting characters. Everything else is convention, and in .NET the convention is followed unusually closely, because the whole base class library was written to it. Code that breaks it looks foreign on sight.
The table
| Kind | Convention | Example |
|---|---|---|
| Class, struct, record, enum, delegate, namespace | PascalCase | InvoiceService |
| Interface | I then PascalCase | IRepository |
| Method, property, event, public field | PascalCase | GetTotal |
| Constant, field or local | PascalCase | MaxAttempts |
| Local variable, parameter | camelCase | lineTotal |
| Private or internal field | _camelCase | _accountId |
| Private static field | s_camelCase | s_hitCount |
| Generic type parameter | T then PascalCase | TEntity |
Two that surprise people
Constants are PascalCase, not SHOUTING_CASE: write MaxAttempts. And multi-letter acronyms are written as words, which is why the library has HttpClient, XmlReader and Id rather than HTTPClient or ID.
Where the rules actually live
The underscore prefix for private fields and the s_ prefix for private static fields come from the .NET runtime team's style. They are not compiler rules. A team that wants them enforced puts naming rules in an .editorconfig file, which the compiler, the editor and dotnet format all read.
Example
Invoice invoice = new Invoice(42); // local variable: camelCase
invoice.AddLine(150, 10);
invoice.AddLine(125, 2);
Console.WriteLine(invoice.GetTotal());
Console.WriteLine(invoice.GetAccountId());
Console.WriteLine(Invoice.MaxLineCount);
class Invoice // type name: PascalCase
{
public const int MaxLineCount = 10; // constant: PascalCase, not MAX_LINE_COUNT
private readonly int _accountId; // private field: leading underscore
private int _total;
public Invoice(int accountId) // parameter: camelCase
{
_accountId = accountId;
}
public void AddLine(int unitPrice, int quantity) // method: PascalCase
{
int lineTotal = unitPrice * quantity; // local: camelCase
_total = _total + lineTotal;
}
public int GetTotal()
{
return _total;
}
public int GetAccountId()
{
return _accountId;
}
}
// Output:
// 1750
// 42
// 10When to use it
- Make a new file look like the rest of a C# codebase on first read
- Tell a private field from a local variable at a glance inside a long method
- Review a pull request for the naming the compiler will never check
- Add an .editorconfig so dotnet format enforces the house style for everyone
More examples
Interfaces, type parameters and static fields
The three prefixes answer a question a reader would otherwise have to scroll for: whether a name is an interface, a type placeholder, or state shared by every instance of the class. The s_ prefix in particular flags shared mutable state, which is where concurrency bugs live.
// An interface name starts with I. A type parameter starts with T.
interface IRepository<TEntity>
{
TEntity? Find(int id);
}
class Cache
{
// Private static field: s_ prefix, from the .NET runtime style.
private static int s_hitCount;
// Private instance field: leading underscore.
private readonly string _name;
public Cache(string name)
{
_name = name;
s_hitCount = s_hitCount + 1;
}
}Constants and a positional record
Constants keep PascalCase whatever their access level, which is the opposite of the habit most other ecosystems have. The record line shows the one place where a parameter is PascalCase: it becomes a public property, so it follows the member rule rather than the parameter rule.
class Retry
{
// PascalCase, including for private constants. .NET does not shout.
public const int MaxAttempts = 3;
private const string KeyPrefix = "retry:";
}
// The parameters of a record are its public properties, so they are PascalCase.
public record PhysicalAddress(string Street, string City, string PostalCode);
// The parameters of a class or struct are ordinary parameters: camelCase.
public class DataService(string connectionString);Acronyms, and names that clash with keywords
Following the library's own casing for acronyms keeps your types sitting next to HttpClient and XmlReader without looking out of place. The verbatim @ identifier exists mainly for interoperating with code generated from other languages or from a JSON schema, where a field really is called class.
class Document
{
public int Id; // not ID
public string? XmlBody; // not XMLBody
public string? HttpEtag; // not HTTPETag
}
// An identifier that collides with a keyword needs an @ prefix. The @ is not
// part of the name. Rename instead, unless you are matching another language.
class Lesson
{
public string? @class;
}
Discussion