Last Updated: May 22, 2026
Coding standards are the conventions that make C# code look and feel the same across files, projects, and teams. Microsoft publishes naming guidelines, the .NET runtime team enforces them in its own source, and most C# codebases follow them closely. This lesson covers the rules you'll meet every day: how to name types and members, how to organize a file, how .editorconfig and dotnet format enforce style automatically, when to write comments, and how to lay out a project so files are easy to find. The deeper questions of idiomatic style (_Effective C# Tips_), clean code design (_Clean Code Principles_), and analyzers (_Analyzers & Code Fixes_) build on this foundation.
Code is read far more often than it's written. A single file might be edited once and read a hundred times by reviewers, debuggers, and future maintainers. When every file in a codebase uses the same naming, the same indentation, and the same ordering, readers stop noticing the surface and focus on the meaning.
Consider two versions of the same class:
And the same idea, conventionally written:
Both compile. Both behave the same way. The second one reads like every other C# file you've seen, which means another developer can scan it in seconds. The first one forces the reader to slow down on every identifier, wondering if price and Price mean different things, if DECIMAL is some keyword they missed, or if in_stock is doing something special.
Standards also reduce review friction. When the team has agreed on a style, code review focuses on logic and design, not on whether to put a brace on a new line. And once the rules live in an .editorconfig file, the editor enforces them silently, so the discussion happens once and never again.
Three concrete payoffs run through the rest of the chapter:
Microsoft's naming guidelines are the closest thing C# has to an official style guide. They've been stable since the early 2000s and the entire .NET base class library (BCL), the set of types like List<T>, Dictionary<K,V>, Console, and String that ship with .NET, follows them. Anyone who reads .NET code reads them too.
The rules split identifiers into two main casing styles:
Order, CalculateTax, ProductCategory.unitPrice, customerEmail, totalAmount.Where each style applies:
| Identifier | Style | Example |
|---|---|---|
| Class, struct, record, enum, interface | PascalCase | Order, ShoppingCart, OrderStatus |
| Public field, property, method, event | PascalCase | Total, PlaceOrder, OrderPlaced |
| Constant (any visibility) | PascalCase | MaxItems, DefaultDiscount |
| Local variable | camelCase | subtotal, customerName |
| Method parameter | camelCase | unitPrice, quantity |
| Private field | _camelCase | _items, _repository |
Two extra prefixes show up in specific places:
I, followed by a PascalCase noun or adjective: IComparable, IEnumerable<T>, IOrderRepository. The prefix is a tradition inherited from COM, but it's universally followed in .NET. A reader who sees IDiscountPolicy knows immediately it's an interface, not a class.T for the most common case, or TName for multiple parameters: List<T>, Dictionary<TKey, TValue>, Func<TInput, TResult>. The prefix keeps type parameters visually distinct from real types in the same signature.A small worked example with each rule applied:
Walk through the names. The interface is IDiscountPolicy (I-prefix). The class implementing it is PercentageDiscount (PascalCase). Its private field _rate uses the _camelCase form so it's visually distinct from a local variable or parameter. The generic class ShoppingCart<TItem> uses TItem instead of plain T to signal what the parameter represents. The constant MaxItems is PascalCase regardless of visibility. The parameter item and the local subtotal are camelCase. Every name follows one of the rules above and nothing else.
readonly, and static readonlyC# has three ways to declare a value that doesn't change after assignment, and each has its own conventions.
A `const` field is a compile-time constant. The value must be known when the code is compiled, and it's baked into every caller that uses it. const is restricted to primitive types (int, string, bool, etc.) plus enums. The naming convention is PascalCase, regardless of access level:
A `readonly` field can be assigned in the declaration or in a constructor, then never again. It's a runtime constant, evaluated when the object is built. Use it when the value isn't known until an object is constructed, or when you need a non-primitive type. The naming convention follows the field's visibility: private readonly fields use _camelCase, public ones use PascalCase.
A `static readonly` field belongs to the type rather than an instance, and it's initialized once when the type is first used. It's the standard choice when you want a "constant" of a non-primitive type, like a pre-built object or collection. Naming is PascalCase, treating it like the constant it logically is:
A practical rule of thumb for when to use which:
| You want... | Use |
|---|---|
| A primitive value known at compile time | const |
| A primitive value computed in a constructor | readonly |
| An immutable object or collection shared across instances | static readonly |
| A reference that won't be reassigned but whose target may mutate | readonly (with care) |
Because const is inlined at the call site, changing a public const in a library forces every dependent project to be recompiled. public static readonly doesn't have this problem, since callers read the field at runtime. For public API surface that might evolve, prefer static readonly.
A C# namespace groups related types. The traditional syntax wraps everything in braces:
Since C# 10, you can declare a file-scoped namespace, which applies to the entire file and saves one level of indentation:
The semicolon at the end of the namespace line is what makes it file-scoped. New C# files should use this form by default. The block form still works and appears in older code, but every Visual Studio and dotnet new template now generates file-scoped namespaces.
The next rule is just as widely followed: one public type per file, and the file name matches the type. Product.cs contains class Product. IDiscountPolicy.cs contains interface IDiscountPolicy. The benefits are practical:
Small helper types tightly coupled to a public type can share its file. The two common cases are nested types and small enums used only by one class:
Putting OrderStatus next to Order is fine because they're used together. Once an enum or helper grows independent users, move it to its own file.
The matching directory layout follows the namespace. The folder structure of a typical project looks like this:
The folder name maps to the namespace segment, the file name maps to the type, and the root namespace usually matches the project name. This convention is what lets tools like Visual Studio's "go to type" search and dotnet new templates work without configuration.
using DirectivesEvery .cs file starts with using directives, which import namespaces so types can be referenced by short name. The convention for ordering them is:
System.* namespaces first, alphabetically.An example:
The grouping helps readers scan the dependencies. Standard library imports come first because they're always there; framework and library imports come next because they're shared across the team; project imports come last because they're the ones that change as you reorganize folders.
Two more rules:
In .NET 6 and later, projects can also declare global usings, which apply to every file in the project:
Several common namespaces (System, System.IO, System.Linq, System.Threading.Tasks, etc.) are imported as global usings automatically when a project has <ImplicitUsings>enable</ImplicitUsings> in its csproj file, which is the default for new templates. This is why modern code samples often skip using System; at the top, the namespace is already in scope project-wide.
Microsoft's C# style uses four spaces for indentation, not tabs. This is consistent with Visual Studio defaults and the .NET runtime source. Mixing tabs and spaces is the most common cause of "it looks fine here but broken in another editor" diffs, so pick one and stick to it (spaces, per Microsoft).
Braces go on their own line, a style sometimes called Allman:
Even single-statement if and for bodies use braces in modern C# style:
The "always braces" rule prevents a class of bugs where someone adds a second line under an if without noticing the missing braces:
What's wrong with this code?
The return 0m; runs regardless of the condition, because only the first statement is part of the if. The indentation is misleading. Always-braces makes this impossible.
Fix:
Line length is less strictly defined. Microsoft's guidelines don't set a hard limit, but most teams choose a soft cap somewhere between 100 and 120 characters. The .NET runtime uses 120. The exact number is less important than the principle: very long lines force horizontal scrolling and break side-by-side diffs. When a line gets too long, break it after a comma, after an operator, or after the opening paren of a method call:
Trailing whitespace at the end of lines should be removed. It serves no purpose, it creates noisy diffs when one editor strips it and another doesn't, and most editors can be set to trim it on save.
.editorconfig and Enforcing StyleAn .editorconfig file lives at the root of a repository (or any folder) and tells editors how to format files. It's not C#-specific; the format works for any language. What makes it powerful in .NET is that the C# compiler and Roslyn analyzers read it too, so the same file that drives your editor's indentation also drives compile-time warnings about style.
A minimal .editorconfig for a C# project looks like this:
The root = true at the top tells the editor not to look further up the directory tree. The [*] section applies to every file. The [*.cs] section adds C#-specific rules. The :warning and :suggestion suffixes set the severity: error fails the build, warning shows a yellow squiggle, suggestion shows a hint, and silent only applies when you explicitly run a fix.
C# naming rules can also live in .editorconfig. The format is verbose but the structure is regular: define a symbol group, define a style, then bind them together as a rule. The rule for "private fields should be _camelCase":
Three pieces fit together: dotnet_naming_symbols.private_fields describes what code elements the rule applies to (private fields). dotnet_naming_style.underscore_prefix describes the desired style (prefix _, then camelCase). dotnet_naming_rule.private_fields_underscore ties the two together and sets a severity. After adding this rule, a violation like private int count; shows up as a warning right in the IDE, with a code fix suggestion to rename it to _count.
To make these style rules count during a build, add this to the csproj:
Without this flag, the IDE shows the violations but dotnet build ignores them. With it, every warning-severity style rule reports during build, and error-severity rules break the build. Teams that care about consistency turn this on so CI catches anything that slipped past local tooling.
The flow looks like this:
The IDE and the build read the same file, so what passes locally also passes in CI. That's the value of .editorconfig: one source of truth that every tool respects. Roslyn analyzers plug into the same pipeline to add deeper rules, but the style and naming rules already live here.
dotnet formatdotnet format is a command-line tool that reads .editorconfig and rewrites files to match. It's the bridge between "the editor knows the rules" and "the rules are applied across the whole codebase." Running it once on a repository before adopting standards normalizes thousands of files at once. Running it in CI catches anything new that violates the rules.
Common ways to invoke it:
A typical workflow looks like this. Before opening a pull request, a developer runs dotnet format to clean up their changes. In CI, the pipeline runs dotnet format --verify-no-changes, which exits non-zero if any file would be modified. If the check fails, the PR can't merge until the developer pulls the fix locally and pushes again.
Two practical notes. First, dotnet format only applies fixes for analyzers that ship with a "code fix provider." A naming rule without a code fix can still produce warnings, but the tool won't rename anything automatically. Second, the first run on a legacy codebase can produce a large diff. Most teams split the cleanup into two commits: one for whitespace and one for style, so reviewers can scan each part independently.
dotnet format rewrites files in place. Run it on a clean working tree (commit or stash first) so a diff shows exactly what changed. Reverting an accidental reformat is much harder than re-running the tool.
Comments fall into two categories: regular // comments meant for whoever reads the code next, and /// XML doc comments that get extracted into IntelliSense and documentation files.
The rule for regular comments is simple: comment why, not what. Code already says what it does. A comment should explain a non-obvious reason for doing it that way.
When a comment is needed to explain what a block of code is doing, that's often a sign the block should be extracted into a named method. The method name then takes the place of the comment, and it stays in sync with the code automatically.
XML doc comments use /// and a small vocabulary of tags. They go on public API: types, methods, properties, events. The compiler can be told to emit them as an XML file alongside the assembly, which is then used by IDEs to power IntelliSense tooltips:
The common tags:
| Tag | Use |
|---|---|
<summary> | One-sentence overview of the member |
<param name="..."> | Description of a parameter |
<returns> | Description of the return value |
<exception cref="..."> | Documents an exception the method may throw |
<remarks> | Longer description, examples, notes |
<see cref="..."> | Cross-reference to another member |
<paramref name="..."> | Reference to a parameter from within prose |
To get the compiler to produce an XML file, add this to the csproj:
Once this is on, the compiler also warns about public members that have no doc comments (warning CS1591). Teams that don't want this noise can suppress CS1591, but the warning is useful as a reminder that a public method's intent isn't documented.
When to skip doc comments:
// comment is enough if anything is needed.internal as a contract across folders.Name doesn't benefit from <summary>Gets the name.</summary>. The summary repeats the name with no added insight, and the XML noise outweighs the value.The shape of the rule: doc comments on public API where the name alone doesn't tell the full story; plain comments inside method bodies where a reader would otherwise have to pause and ask "why?"; nothing when the code speaks for itself.
A single project has one csproj file, a handful of folders, and many .cs files. The way those files are arranged shapes how the codebase feels as it grows. There are two common organizing principles.
By feature (recommended for most projects). Group files by what they do, not how they do it. A Catalog folder contains everything related to products and categories. An Orders folder contains orders, statuses, repositories, and order-related services. A Customers folder contains customers, addresses, and contact info.
The advantage is locality. To change how orders work, you open one folder and most of what you need is there. Adding a new feature creates a new folder rather than scattering files across the project.
By layer (still common, especially in older projects). Group files by their architectural role: Models/ for data classes, Services/ for business logic, Repositories/ for data access, Controllers/ for HTTP endpoints. This is the default in many tutorials and works fine for small projects, but as features grow it forces a single change to span every layer's folder.
For a project of any meaningful size, organize by feature first and let layers exist within each feature folder if they earn their keep. The conventional rule: if a folder has fewer than three files, it probably doesn't need to exist; if it has more than fifteen, it should probably be split.
A solution often contains multiple projects, each a separate csproj. A typical e-commerce setup might look like:
| Project | Purpose |
|---|---|
ECommerce.Domain | Core types (Product, Order, Customer) and business rules |
ECommerce.Infrastructure | Repositories, database access, external API clients |
ECommerce.Application | Use cases that orchestrate domain objects |
ECommerce.Api | Web API controllers, exposes Application over HTTP |
ECommerce.Tests | Unit and integration tests |
Each project has its own folder under the solution root, its own root namespace matching the project name, and references the projects it depends on. The dependency direction is one-way: Api depends on Application, which depends on Domain. Domain depends on nothing in the solution. Tools like dotnet list reference and analyzers can enforce this.
A few file-level rules that fit anywhere:
Product.cs contains Product. IOrderRepository.cs contains IOrderRepository.OrderTests.cs lives in ECommerce.Tests/Orders/, mirroring the production folder.Generated/) and don't get edited by hand.AssemblyInfo.cs (in older projects) or files holding [assembly: ...] attributes live here. Modern SDK-style projects put most of this in the csproj instead.The same principles apply at every scale: a folder should answer one question, a file should hold one type, and the structure should match how you'd describe the project out loud. A new teammate who reads "the orders module has a repository and a service" should find exactly that under Orders/.
A small but realistic slice of an e-commerce project that demonstrates every rule from this chapter. The folder layout looks like this:
The contents of Orders/IDiscountPolicy.cs:
The contents of Orders/PercentageDiscount.cs:
The contents of Orders/OrderStatus.cs:
The contents of Orders/OrderItem.cs:
The contents of Orders/Order.cs:
A small driver program that exercises the slice:
Walk back through what every rule contributed:
Order, Total, MarkShipped), and the MaxItems constant.productId, unitPrice, rate) and local variables (subtotal, total)._items and _rate, making them visually distinct from parameters._items and _rate because they're set once and never reassigned.System.*, then project namespaces, with a blank line between groups.Orders/, Catalog/, Customers/) rather than by layer.The cumulative effect is more than each rule on its own. The reader doesn't have to think about formatting at all. They can focus on the question that actually matters: does the code do the right thing?