.NET Coding Conventions - C# (original) (raw)

Coding conventions are essential for maintaining code readability, consistency, and collaboration within a development team. Code that follows industry practices and established guidelines is easier to understand, maintain, and extend. Most projects enforce a consistent style through code conventions. The dotnet/docs and dotnet/samples projects are no exception. In this series of articles, you learn our coding conventions and the tools we use to enforce them. You can take our conventions as-is, or modify them to suit your team's needs.

We chose our conventions based on the following goals:

  1. Correctness: Our samples are copied and pasted into your applications. We expect that, so we need to make code that's resilient and correct, even after multiple edits.
  2. Teaching: The purpose of our samples is to teach all of .NET and C#. For that reason, we don't place restrictions on any language feature or API. Instead, those samples teach when a feature is a good choice.
  3. Consistency: Readers expect a consistent experience across our content. All samples should conform to the same style.
  4. Adoption: We aggressively update our samples to use new language features. That practice raises awareness of new features, and makes them more familiar to all C# developers.

Important

These guidelines are used by Microsoft to develop samples and documentation. They were adopted from the .NET Runtime, C# Coding Style and C# compiler (roslyn) guidelines. We chose those guidelines because of their adoption over several years of Open Source development. These guidelines help community members participate in the runtime and compiler projects. They're meant to be an example of common C# conventions, and not an authoritative list (see Framework Design Guidelines for detailed guidelines).

The teaching and adoption goals are why the docs coding convention differs from the runtime and compiler conventions. Both the runtime and compiler have strict performance metrics for hot paths. Many other applications don't. Our teaching goal mandates that we don't prohibit any construct. Instead, samples show when constructs should be used. We update samples more aggressively than most production applications do. Our adoption goal mandates that we show code you should write today, even when code written last year doesn't need changes.

This article explains our guidelines. The guidelines evolve over time, and you'll find samples that don't follow our guidelines. We welcome PRs that bring those samples into compliance, or issues that draw our attention to samples we should update. Our guidelines are Open Source and we welcome PRs and issues. However, if your submission would change these recommendations, open an issue for discussion first. You're welcome to use our guidelines, or adapt them to your needs.

Tools can help your team enforce your conventions. You can enable code analysis to enforce the rules you prefer. You can also create an editorconfig so that Visual Studio automatically enforces your style guidelines. As a starting point, you can copy the dotnet/docs .editorconfig to use our style.

These tools make it easier for your team to adopt your preferred guidelines. Visual Studio applies the rules in all .editorconfig files in scope to format your code. You can use multiple configurations to enforce corporate-wide conventions, team conventions, and even granular project conventions.

Code analysis produces warnings and diagnostics when it detects rule violations. You configure the rules you want applied to your project. Then, each CI build notifies developers when they violate any of the rules.

Diagnostic IDs

Language guidelines

The following sections describe practices that the .NET docs team follows to prepare code examples and samples. In general, follow these practices:

More specific guidelines follow.

String data

string displayName = $"{nameList[n].LastName}, {nameList[n].FirstName}";  
var phrase = "lalalalalalalalalalalalalalalalalalalalalalalalalalalalalala";  
var manyPhrases = new StringBuilder();  
for (var i = 0; i < 10000; i++)  
{  
    manyPhrases.Append(phrase);  
}  
//Console.WriteLine("tra" + manyPhrases);  
var message = """  
    This is a long message that spans across multiple lines.  
    It uses raw string literals. This means we can  
    also include characters like \n and \t without escaping them.  
    """;  
// Execute the queries.  
Console.WriteLine("scoreQuery:");  
foreach (var student in scoreQuery)  
{  
    Console.WriteLine($"{student.Last} Score: {student.score}");  
}  

Constructors and initialization

public record Person(string FirstName, string LastName);  
public class LabelledContainer<T>(string label)  
{  
    public string Label { get; } = label;  
    public required T Contents  
    {  
        get;  
        init;  
    }  
}  

Arrays and collections

string[] vowels = [ "a", "e", "i", "o", "u" ];

Delegates

Action<string> actionExample1 = x => Console.WriteLine($"x is: {x}");

Action<string, string> actionExample2 = (x, y) =>
    Console.WriteLine($"x is: {x}, y is {y}");

Func<string, int> funcExample1 = x => Convert.ToInt32(x);

Func<int, int, int> funcExample2 = (x, y) => x + y;
actionExample1("string for x");

actionExample2("string for x", "string for y");

Console.WriteLine($"The value is {funcExample1("1")}");

Console.WriteLine($"The sum is {funcExample2(1, 2)}");
public delegate void Del(string message);  
public static void DelMethod(string str)  
{  
    Console.WriteLine($"DelMethod argument: {str}");  
}  
Del exampleDel2 = DelMethod;  
exampleDel2("Hey");  
Del exampleDel1 = new Del(DelMethod);  
exampleDel1("Hey");  

try-catch and using statements in exception handling

static double ComputeDistance(double x1, double y1, double x2, double y2)  
{  
    try  
    {  
        return Math.Sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));  
    }  
    catch (System.ArithmeticException ex)  
    {  
        Console.WriteLine($"Arithmetic overflow or underflow: {ex}");  
        throw;  
    }  
}  
Font bodyStyle = new Font("Arial", 10.0f);  
try  
{  
    byte charset = bodyStyle.GdiCharSet;  
}  
finally  
{  
    bodyStyle?.Dispose();  
}  

You can do the same thing with a using statement.

using (Font arial = new Font("Arial", 10.0f))  
{  
    byte charset2 = arial.GdiCharSet;  
}  

Use the new using syntax that doesn't require braces:

using Font normalStyle = new Font("Arial", 10.0f);  
byte charset3 = normalStyle.GdiCharSet;  

&& and || operators

Console.Write("Enter a dividend: ");  
int dividend = Convert.ToInt32(Console.ReadLine());  
Console.Write("Enter a divisor: ");  
int divisor = Convert.ToInt32(Console.ReadLine());  
if ((divisor != 0) && (dividend / divisor) is var result)  
{  
    Console.WriteLine($"Quotient: {result}");  
}  
else  
{  
    Console.WriteLine("Attempted division by 0 ends up here.");  
}  

If the divisor is 0, the second clause in the if statement would cause a run-time error. But the && operator short-circuits when the first expression is false. That is, it doesn't evaluate the second expression. The & operator would evaluate both, resulting in a run-time error when divisor is 0.

new operator

var firstExample = new ExampleClass();  
ExampleClass instance2 = new();  

The preceding declarations are equivalent to the following declaration.

ExampleClass secondExample = new ExampleClass();  
var thirdExample = new ExampleClass { Name = "Desktop", ID = 37414,  
    Location = "Redmond", Age = 2.3 };  

The following example sets the same properties as the preceding example but doesn't use initializers.

var fourthExample = new ExampleClass();  
fourthExample.Name = "Desktop";  
fourthExample.ID = 37414;  
fourthExample.Location = "Redmond";  
fourthExample.Age = 2.3;  

Event handling

public Form2()
{
    this.Click += (s, e) =>
        {
            MessageBox.Show(
                ((MouseEventArgs)e).Location.ToString());
        };
}

The lambda expression shortens the following traditional definition.

public Form1()
{
    this.Click += new EventHandler(Form1_Click);
}

void Form1_Click(object? sender, EventArgs e)
{
    MessageBox.Show(((MouseEventArgs)e).Location.ToString());
}

Static members

Call static members by using the class name: ClassName.StaticMember. This practice makes code more readable by making static access clear. Don't qualify a static member defined in a base class with the name of a derived class. While that code compiles, the code readability is misleading, and the code might break in the future if you add a static member with the same name to the derived class.

LINQ queries

var seattleCustomers = from customer in Customers  
                       where customer.City == "Seattle"  
                       select customer.Name;  
var localDistributors =  
    from customer in Customers  
    join distributor in Distributors on customer.City equals distributor.City  
    select new { Customer = customer, Distributor = distributor };  
var localDistributors2 =  
    from customer in Customers  
    join distributor in Distributors on customer.City equals distributor.City  
    select new { CustomerName = customer.Name, DistributorName = distributor.Name };  
var seattleCustomers = from customer in Customers  
                       where customer.City == "Seattle"  
                       select customer.Name;  
var seattleCustomers2 = from customer in Customers  
                        where customer.City == "Seattle"  
                        orderby customer.Name  
                        select customer;  
var scoreQuery = from student in students  
                 from score in student.Scores  
                 where score > 90  
                 select new { Last = student.LastName, score };  

Implicitly typed local variables

var message = "This is clearly a string.";  
var currentTemperature = 27;  
int numberOfIterations = Convert.ToInt32(Console.ReadLine());  
int currentMaximum = ExampleClass.ResultSoFar();  
var inputInt = Console.ReadLine();  
Console.WriteLine(inputInt);  
var phrase = "lalalalalalalalalalalalalalalalalalalalalalalalalalalalalala";  
var manyPhrases = new StringBuilder();  
for (var i = 0; i < 10000; i++)  
{  
    manyPhrases.Append(phrase);  
}  
//Console.WriteLine("tra" + manyPhrases);  
foreach (char ch in laugh)  
{  
    if (ch == 'h')  
    {  
        Console.Write("H");  
    }  
    else  
    {  
        Console.Write(ch);  
    }  
}  
Console.WriteLine();  

Some of our samples explain the natural type of an expression. Those samples must use var so that the compiler picks the natural type. Even though those examples are less obvious, the use of var is required for the sample. The text should explain the behavior.

File scoped namespace declarations

Most code files declare a single namespace. Therefore, our examples should use the file scoped namespace declarations:

namespace MySampleCode;

Place the using directives outside the namespace declaration

When a using directive is outside a namespace declaration, that imported namespace is its fully qualified name. The fully qualified name is clearer. When the using directive is inside the namespace, it could be either relative to that namespace, or its fully qualified name.

using Azure;

namespace CoolStuff.AwesomeFeature
{
    public class Awesome
    {
        public void Stuff()
        {
            WaitUntil wait = WaitUntil.Completed;
            // ...
        }
    }
}

Assuming there's a reference (direct, or indirect) to the WaitUntil class.

Now, let's change it slightly:

namespace CoolStuff.AwesomeFeature
{
    using Azure;

    public class Awesome
    {
        public void Stuff()
        {
            WaitUntil wait = WaitUntil.Completed;
            // ...
        }
    }
}

And it compiles today. And tomorrow. But then sometime next week the preceding (untouched) code fails with two errors:

- error CS0246: The type or namespace name 'WaitUntil' could not be found (are you missing a using directive or an assembly reference?)
- error CS0103: The name 'WaitUntil' does not exist in the current context

One of the dependencies introduced this class in a namespace then ends with .Azure:

namespace CoolStuff.Azure
{
    public class SecretsManagement
    {
        public string FetchFromKeyVault(string vaultId, string secretId) { return null; }
    }
}

A using directive placed inside a namespace is context-sensitive and complicates name resolution. In this example, it's the first namespace that it finds.

Adding a new namespace that matches either CoolStuff.Azure or CoolStuff.AwesomeFeature.Azure would match before the global Azure namespace. You could resolve it by adding the global:: modifier to the using declaration. However, it's easier to place using declarations outside the namespace instead.

namespace CoolStuff.AwesomeFeature
{
    using global::Azure;

    public class Awesome
    {
        public void Stuff()
        {
            WaitUntil wait = WaitUntil.Completed;
            // ...
        }
    }
}

Style guidelines

In general, use the following format for code samples:

// The following declaration creates a query. It does not run  
// the query.  

Layout conventions

Good layout uses formatting to emphasize the structure of your code and to make the code easier to read. Microsoft examples and samples conform to the following conventions:

if ((startX > endX) && (startX > previousX))  
{  
    // Take appropriate action.  
}  

Exceptions are when the sample explains operator or expression precedence.

Security

Follow the guidelines in Secure Coding Guidelines.