Table of Contents

.NET 编码规范

❗ .NET 6 之后,SDK内引入了 dotnet format 工具,可以使用该工具调整代码格式中的基本问题,例如换行和空格的问题。 在CI流程中,将使用 dotnet format 进行的代码格式检查,检查不通过则无法进行构建和合并。请注意在提交前进行代码格式的整理。

ref: https://github.com/dotnet/aspnetcore/wiki/Engineering-guidelines#coding-guidelines

通用编码风格规范

绝大部分的编码规范使用VS默认的代码格式即可,优先考虑可读性。

  1. 使用4个空格缩进,而不是tab
  2. 私有字段使用_camelCase格式
  3. 如非必要,省略this.
  4. 总是声明成员的作用范围,即便是默认的 (例如. private string _foo; 而非 string _foo;)
  5. 左花括号 ({) 新起一行
  6. 使用语言特性的时候(表达体成员,throw表达式,元组等),尽可能的保持代码的可读性,方便管理。
    • 一个糟糕的例子:
public (int, string) GetData(string filter) => (Data.Status, Data.GetWithFilter(filter ?? throw new ArgumentNullException(nameof(filter))));

关键词var的使用

在编译器允许的情况下,尽可能使用var关键字。例如,正确的使用方法:

var fruit = "Lychee";
var fruits = new List<Fruit>();
var flavor = fruit.GetFlavor();
string fruit = null; // can't use "var" because the type isn't known (though you could do (string)null, don't!)
const string expectedName = "name"; // can't use "var" with const

错误的使用方法:

string fruit = "Lychee";
List<Fruit> fruits = new List<Fruit>();
FruitFlavor flavor = fruit.GetFlavor();

使用C#的类型关键词取代的.Net的类型名称

使用类型的时候,尽可能使用C#提供的类型关键字,用以取代.Net的类型名。 例如, 正确的使用方法:

public string TrimString(string s) {
    return string.IsNullOrEmpty(s)
        ? null
        : s.Trim();
}

var intTypeName = nameof(Int32); // can't use C# type keywords with nameof

错误的使用方法:

public String TrimString(String s) {
    return String.IsNullOrEmpty(s)
        ? null
        : s.Trim();
}

跨平台编码

项目构架基于支持多平台操作系统的 CoreClR,所以不要假定在Windows平台上运行,编码的时候需要考虑不同的操作系统。 这里有一些建议。 换行 Windows 使用 \r\n, OS X and Linux 使用 \n. 需要注意的时候, 使用 Environment.NewLine 替代硬编码的换行符.

Note: 这一点不一定是必须的.

但是需要注意,当代码中使用@""文本块包含换行符的时候,行尾可能会有些问题。

C#11 (.NET 7) 之后可以使用 Raw string literal 来处理字符串插值。

环境变量

不同的操作系统使用不同的变量名去表示相似的配置。代码的时候可能需要注意这些差异。

例如,在查询用户主目录的时候,在Windows上使用的是USERPROFILE,但是在大部分的Linux系统上使用的是HOME

var homeDir = Environment.GetEnvironmentVariable("USERPROFILE") 
                  ?? Environment.GetEnvironmentVariable("HOME");

文件路径分隔符

Windows 使用 \ ,而 OS X 和 Linux 使用 / 分割目录. 可以使用Path.Combine() 或 Path.DirectorySeparatorChar取代硬编码的斜杠。

如果用不上的时候(例如在脚本中),那就尽可能的使用/,Windows相比Linux会更加包容一些。

When to use internals vs. public and when to use InternalsVisibleTo

Use InternalsVisibleTo when sharing code between types in the same assembly, same feature area, or to unit test internal types and members. If two runtime assemblies need to share common helpers then we will use a "shared source" solution with build-time only packages. Check out the some of the projects in https://github.com/aspnet/Common/ and how they are referenced from other solutions.

If two runtime assemblies need to call each other's APIs, consider making the APIs public or if there's enough extensibility for a customer to perform something similar. If we need it, it is likely that our customers need it.

异步方法模式

默认情况下,所有的异步方法都需要加上Async后缀。

Passing cancellation tokens is done with an optional parameter with a value of default(CancellationToken), which is equivalent to CancellationToken.None (one of the few places that we use optional parameters). The main exception to this is in web scenarios where there is already an HttpContext being passed around, in which case the context has its own cancellation token that can be used when needed.

Sample async method:

public Task GetDataAsync(
    QueryParams query,
    int maxData,
    CancellationToken cancellationToken = default(CancellationToken))
{
    ...
}

扩展方法模式

The general rule is: if a regular static method would suffice, avoid extension methods.

Extension methods are often useful to create chainable method calls, for example, when constructing complex objects, or creating queries.

Internal extension methods are allowed, but bear in mind the previous guideline: ask yourself if an extension method is truly the most appropriate pattern.

The namespace of the extension method class should generally be the namespace that represents the functionality of the extension method, as opposed to the namespace of the target type. One common exception to this is that the namespace for middleware extension methods is normally always the same is the namespace of IAppBuilder.

The class name of an extension method container (also known as a "sponsor type") should generally follow the pattern of <Feature>Extensions, <Target><Feature>Extensions, or <Feature><Target>Extensions. For example:

namespace Food {
    class Fruit { ... }
}

namespace Fruit.Eating {
    class FruitExtensions { public static void Eat(this Fruit fruit); }
  OR
    class FruitEatingExtensions { public static void Eat(this Fruit fruit); }
  OR
    class EatingFruitExtensions { public static void Eat(this Fruit fruit); }
}

When writing extension methods for an interface the sponsor type name must not start with an I.

文档注释

针对所有的公开接口都需要写文档说明,包括公开的类型,即使用public修改的方法和类型,非公开的则不需要。注释由代码的编写者进行撰写。

Note: Public means callable by a customer, so it includes protected APIs. However, some public APIs might still be "for internal use only" but need to be public for technical reasons. We will still have doc comments for these APIs but they will be documented as appropriate.

单元测试和功能测试

程序集命名

The unit tests for the Microsoft.Fruit assembly live in the Microsoft.Fruit.Tests assembly.

The functional tests for the Microsoft.Fruit assembly live in the Microsoft.Fruit.FunctionalTests assembly.

In general there should be exactly one unit test assembly for each product runtime assembly. In general there should be one functional test assembly per repo. Exceptions can be made for both.

单元测试类命名

Test class names end with Test and live in the same namespace as the class being tested. For example, the unit tests for the Microsoft.Fruit.Banana class would be in a Microsoft.Fruit.BananaTest class in the test assembly.

单元测试方法命名

Unit test method names must be descriptive about what is being tested, under what conditions, and what the expectations are. Pascal casing and underscores can be used to improve readability. The following test names are correct:

PublicApiArgumentsShouldHaveNotNullAnnotation
Public_api_arguments_should_have_not_null_annotation

The following test names are incorrect:

Test1
Constructor
FormatString
GetData

单元测试结构

The contents of every unit test should be split into three distinct stages, optionally separated by these comments:

// Arrange  
// Act  
// Assert 

The crucial thing here is that the Act stage is exactly one statement. That one statement is nothing more than a call to the one method that you are trying to test. Keeping that one statement as simple as possible is also very important. For example, this is not ideal:

int result = myObj.CallSomeMethod(GetComplexParam1(), GetComplexParam2(), GetComplexParam3());

This style is not recommended because way too many things can go wrong in this one statement. All the GetComplexParamN() calls can throw for a variety of reasons unrelated to the test itself. It is thus unclear to someone running into a problem why the failure occurred.

The ideal pattern is to move the complex parameter building into the Arrange section:

// Arrange
P1 p1 = GetComplexParam1();
P2 p2 = GetComplexParam2();
P3 p3 = GetComplexParam3();

// Act
int result = myObj.CallSomeMethod(p1, p2, p3);

// Assert
Assert.AreEqual(1234, result);

Now the only reason the line with CallSomeMethod() can fail is if the method itself blew up. This is especially important when you're using helpers such as ExceptionHelper, where the delegate you pass into it must fail for exactly one reason.

单元测试异常信息

In general testing the specific exception message in a unit test is important. This ensures that the exact desired exception is what is being tested rather than a different exception of the same type. In order to verify the exact exception it is important to verify the message.

To make writing unit tests easier it is recommended to compare the error message to the RESX resource. However, comparing against a string literal is also permitted.

var ex = Assert.Throws<InvalidOperationException>(
    () => fruitBasket.GetBananaById(1234));
Assert.Equal(
    Strings.FormatInvalidBananaID(1234),
    ex.Message);

Use xUnit.net's plethora of built-in assertions

xUnit.net includes many kinds of assertions – please use the most appropriate one for your test. This will make the tests a lot more readable and also allow the test runner report the best possible errors (whether it's local or the CI machine). For example, these are bad:

Assert.Equal(true, someBool);

Assert.True("abc123" == someString);

Assert.True(list1.Length == list2.Length);

for (int i = 0; i < list1.Length; i++) {
    Assert.True(
        String.Equals
            list1[i],
            list2[i],
            StringComparison.OrdinalIgnoreCase));
}

These are good:

Assert.True(someBool);

Assert.Equal("abc123", someString);

// built-in collection assertions!
Assert.Equal(list1, list2, StringComparer.OrdinalIgnoreCase);

并行测试

By default all unit test assemblies should run in parallel mode, which is the default. Unit tests shouldn't depend on any shared state, and so should generally be runnable in parallel. If the tests fail in parallel, the first thing to do is to figure out why; do not just disable parallel tests!

For functional tests it is reasonable to disable parallel tests.

Use only complete words or common/standard abbreviations in public APIs

Public namespaces, type names, member names, and parameter names must use complete words or common/standard abbreviations.

These are correct:

public void AddReference(AssemblyReference reference);
public EcmaScriptObject SomeObject { get; }

These are incorrect:

public void AddRef(AssemblyReference ref);
public EcmaScriptObject SomeObj { get; }

通用模式

This section contains common patterns used in our code.

日志模式

  1. Always specify an EventId. Include a numeric ID and a name. The name should be a PascalCasedCompoundWord (i.e. no spaces, and each "word" within the name starts with a capital letter).

  2. In production code, use "pre-compiled logging functions" (see below). Test code can use any kind of logging

  3. Prefer defining pre-compiled messages in a static class named Log that is a nested class within the class you are logging from. Messages that are used by multiple components can be defined in a shared class (but this is discouraged).

  4. Consider separating the Log nested class into a separate file by using partial classes. Name the additional file [OriginalClassName].Log.cs

  5. Never use string-interpolation ($"Foo {bar}") for log messages. Log message templates are designed so that structured logging systems can make individual parameters queryable and string-interpolation breaks this.

  6. Always use PascalCasedCompoundWords as template replacement tokens.

Pre-compiled Logging Functions

Production code should use "pre-compiled" logging functions. This means using LoggerMessage.Define to create a compiled function that can be used to perform logging. For example, consider the following log statement:

public class MyComponent
{
    public void MyMethod()
    {
        _logger.LogError(someException, new EventId(1, "ABadProblem"), "You are having a bad problem and will not go to {Location} today", "Space");
    }
}

The logging infrastructure has to parse the template ("You are having a bad problem and will not go to {Location} today") every time the log is written. A pre-compiled logging function allows you to compile the template once and get back a delegate that can be invoked to log the message without requiring the template be re-parsed. For example:

public class MyComponent
{
    public void MyMethod()
    {
        Log.ABadProblem(_logger, "Space", someException);
    }

    private static class Log
    {
        private static readonly Action<ILogger, string, Exception> _aBadProblem =
            LoggerMessage.Define<string>(
                LogLevel.Error,
                new EventId(2, "ABadProblem"), 
                "You are having a bad problem and will not go to {Location} today");

        public static void ABadProblem(ILogger logger, string location, Exception ex) => _aBadProblem(logger, location, ex);
    }
}

If MyComponent is a large class, consider splitting the Log nested class into a separate file:

MyComponent.cs

public partial class MyComponent
{
    public void MyMethod()
    {
        Log.ABadProblem(_logger, "Space", someException);
    }
}

MyComponent.Log.cs

public partial class MyComponent
{
    private static class Log
    {
        private static readonly Action<ILogger, string, Exception> _aBadProblem =
            LoggerMessage.Define<string>(
                LogLevel.Error,
                new EventId(2, "ABadProblem"), 
                "You are having a bad problem and will not go to {Location} today");

        public static void ABadProblem(ILogger logger, string location, Exception ex) => _aBadProblem(logger, location, ex);
    }
}